diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ddcb72b0..00000000 --- a/.coveragerc +++ /dev/null @@ -1,16 +0,0 @@ -# Coverage isn't really compatible with subprocesses so results are unreliable - -[run] -branch = True -#concurrency = multiprocessing -source = ocrmypdf/ - -[report] -exclude_lines = - pragma: no cover - def __repr__ - raise AssertionError - raise NotImplementedError - if 0: - if False: - if __name__ == .__main__.: diff --git a/.docker/Dockerfile b/.docker/Dockerfile index ebb63ddd..4700004e 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,16 +1,62 @@ # OCRmyPDF # -FROM ubuntu:18.04 +FROM ubuntu:20.04 as base + +FROM base as builder + +ENV LANG=C.UTF-8 RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential autoconf automake libtool \ libleptonica-dev \ zlib1g-dev \ - libexempi3 \ - ocrmypdf \ + python3-dev \ + python3-distutils \ + libffi-dev \ + libqpdf-dev \ + ca-certificates \ + curl \ + git + +# Get the latest pip (Ubuntu version doesn't support manylinux2010) +RUN \ + curl https://bootstrap.pypa.io/get-pip.py | python3 + +# Compile and install jbig2 +# Needs libleptonica-dev, zlib1g-dev +RUN \ + mkdir jbig2 \ + && curl -L https://github.com/agl/jbig2enc/archive/ea6a40a.tar.gz | \ + tar xz -C jbig2 --strip-components=1 \ + && cd jbig2 \ + && ./autogen.sh && ./configure && make && make install \ + && cd .. \ + && rm -rf jbig2 + +COPY . /app + +WORKDIR /app + +RUN pip3 install --no-cache-dir \ + -r requirements/main.txt \ + -r requirements/webservice.txt \ + -r requirements/test.txt \ + -r requirements/watcher.txt \ + . + +FROM base + +ENV LANG=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ghostscript \ + img2pdf \ + liblept5 \ + libsm6 libxext6 libxrender-dev \ + zlib1g \ pngquant \ - python3-pip \ - python3-venv \ + python3 \ + qpdf \ tesseract-ocr \ tesseract-ocr-chi-sim \ tesseract-ocr-deu \ @@ -19,54 +65,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ tesseract-ocr-por \ tesseract-ocr-spa \ unpaper \ - wget + && rm -rf /var/lib/apt/lists/* +WORKDIR /app -ENV LANG=C.UTF-8 +COPY --from=builder /usr/local/lib/ /usr/local/lib/ +COPY --from=builder /usr/local/bin/ /usr/local/bin/ -# Compile and install jbig2 -# Needs libleptonica-dev, zlib1g-dev -RUN \ - mkdir jbig2 \ - && wget -q https://github.com/agl/jbig2enc/archive/0.29.tar.gz -O - | \ - tar xz -C jbig2 --strip-components=1 \ - && cd jbig2 \ - && ./autogen.sh && ./configure && make && make install \ - && cd .. \ - && rm -rf jbig2 +COPY --from=builder /app/misc/webservice.py /app/ +COPY --from=builder /app/misc/watcher.py /app/ -RUN apt-get remove -y autoconf automake libtool +# Copy minimal project files to get the test suite. +COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/ +COPY --from=builder /app/requirements /app/requirements +COPY --from=builder /app/tests /app/tests -RUN python3 -m venv --system-site-packages /appenv - -# This installs the latest binary wheel instead of the code in the current -# folder. Installing from source will fail, apparently because cffi needs -# build-essentials (gcc) to do a source installation -# (i.e. "pip install ."). It's unclear to me why this is the case. -RUN . /appenv/bin/activate; \ - pip install --upgrade pip \ - && pip install --upgrade ocrmypdf - -# Now copy the application in, mainly to get the test suite. -# Do this now to make the best use of Docker cache. -COPY . /application -RUN . /appenv/bin/activate; \ - pip install -r /application/requirements/test.txt - -# Remove the junk, including the source version of application since it was -# already installed -RUN rm -rf /tmp/* /var/tmp/* /root/* /application/ocrmypdf \ - && apt-get remove -y build-essential \ - && apt-get autoremove -y \ - && apt-get autoclean -y - -RUN useradd docker \ - && mkdir /home/docker \ - && chown docker:docker /home/docker - -USER docker -WORKDIR /home/docker - -# Must use array form of ENTRYPOINT -# Non-array form does not append other arguments, because that is "intuitive" -ENTRYPOINT ["/application/.docker/docker-wrapper.sh"] +ENTRYPOINT ["/usr/local/bin/ocrmypdf"] diff --git a/.docker/alpine.dockerfile b/.docker/alpine.dockerfile deleted file mode 100644 index d54951f6..00000000 --- a/.docker/alpine.dockerfile +++ /dev/null @@ -1,84 +0,0 @@ -FROM alpine:3.9 as base - -FROM base as builder - -ENV LANG=C.UTF-8 - -RUN \ - echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories \ - # Add runtime dependencies - && apk add --update \ - python3-dev \ - py3-setuptools \ - jbig2enc@testing \ - ghostscript \ - qpdf \ - tesseract-ocr \ - unpaper \ - pngquant \ - libxml2-dev \ - libxslt-dev \ - zlib-dev \ - qpdf-dev \ - libffi-dev \ - leptonica-dev \ - binutils \ - # Install pybind11 for pikepdf - && pip3 install pybind11 \ - # Install flask for the webservice - && pip3 install flask \ - # Add build dependencies - && apk add --virtual build-dependencies \ - build-base \ - git - -COPY . /app - -WORKDIR /app - -RUN pip3 install . - -FROM base - -ENV LANG=C.UTF-8 - -RUN \ - echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories \ - # Add runtime dependencies - && apk add --update \ - python3 \ - jbig2enc@testing \ - ghostscript \ - qpdf \ - tesseract-ocr \ - tesseract-ocr-data-deu \ - tesseract-ocr-data-chi_sim \ - unpaper \ - pngquant \ - libxml2 \ - libxslt \ - zlib \ - qpdf \ - libffi \ - leptonica-dev \ - binutils \ - && mkdir /app - -WORKDIR /app - -# Copy build artifacts (python site-packages9 -COPY --from=builder /usr/lib/python3.6/site-packages /usr/lib/python3.6/site-packages -COPY --from=builder /usr/bin/ocrmypdf /usr/bin/dumppdf.py /usr/bin/latin2ascii.py /usr/bin/pdf2txt.py /usr/bin/img2pdf /usr/bin/chardetect /usr/bin/ - -# Copy -COPY --from=builder /app/.docker/webservice.py /app/ - -# Copy minimal project files to get the test suite. -COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/ -COPY --from=builder /app/requirements /app/requirements -COPY --from=builder /app/tests /app/tests -COPY --from=builder /app/src /app/src -# Copy PKG-INFO from build artifact in app dir to make setuptools-scm happy -RUN cp /usr/lib/python3.6/site-packages/ocrmypdf-*.egg-info/PKG-INFO /app - -ENTRYPOINT ["/usr/bin/ocrmypdf"] diff --git a/.docker/docker-wrapper.sh b/.docker/docker-wrapper.sh deleted file mode 100755 index ecc1af3e..00000000 --- a/.docker/docker-wrapper.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -. /appenv/bin/activate -cd /home/docker -exec ocrmypdf "$@" \ No newline at end of file diff --git a/.docker/polyglot.dockerfile b/.docker/polyglot.dockerfile deleted file mode 100644 index c837a197..00000000 --- a/.docker/polyglot.dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# OCRmyPDF polyglot -# -FROM jbarlow83/ocrmypdf:latest - -USER root - -# Update system and install our dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - tesseract-ocr-all - -RUN apt-get autoremove -y && apt-get clean -y - -USER docker - -# Must use array form of ENTRYPOINT -# Non-array form does not append other arguments, because that is "intuitive" -ENTRYPOINT ["/application/.docker/docker-wrapper.sh"] \ No newline at end of file diff --git a/.docker/webservice.dockerfile b/.docker/webservice.dockerfile deleted file mode 100644 index cd0f71be..00000000 --- a/.docker/webservice.dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# OCRmyPDF webservice -# -FROM jbarlow83/ocrmypdf-polyglot:latest - -USER root - -# Update system and install our dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3-flask - -RUN apt-get autoremove -y && apt-get clean -y - -EXPOSE 5000 - -COPY .docker/webservice.py /application - -USER docker - -VOLUME ["/config"] - -# This config file is optional -ENV OCRMYPDF_WEBSERVICE_SETTINGS "/config/config.py" - -ENTRYPOINT ["python3", "/application/webservice.py"] diff --git a/.dockerignore b/.dockerignore index ee63ddca..2879b33a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,26 +1,44 @@ +# dotfiles +.* +!.coveragerc +!.dockerignore +!.git_archival.txt +!.gitattributes +!.gitignore +!.pre-commit-config.yaml +!.readthedocs.yml + +# Dev scratch *.ipynb -*.pdf -*.pyc -*.rst -*.sublime* **/*.pyc -.*/ -!.git/ -!.docker/ -.ruffus_history.sqlite -bin/ -build/ -docs/ -dist/ -htmlcov/ -include/ -lib/ -MANIFEST.in -ocrmypdf.egg-info/ -staging/ -tests/cache/ -tests/output/ +/*.pdf +/*.qdf +/*.png +/scratch.py +IDEAS +log/ tests/resources/private/ tmp/ venv*/ +/debug_tests.py +*.traineddata +/private + +# Package building +*.egg-info/ +build/ +dist/ wheelhouse/ +pip-wheel-metadata/ + +# Code coverage +htmlcov/ + +# Docker specific +bin/ +docs/ +include/ +lib/ + +# Docker include .git/ +!.git/ diff --git a/.gitattributes b/.gitattributes index 7dab7cbc..928cb783 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,5 +9,6 @@ *.png binary *.jpg binary *.bin binary +*.afdesign binary .git_archival.txt export-subst diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..f9e58e51 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: james-barlow +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/1-general-issues.md b/.github/ISSUE_TEMPLATE/1-general-issues.md new file mode 100644 index 00000000..3aa3c2d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-general-issues.md @@ -0,0 +1,32 @@ +--- +name: General issues +about: Installation, packages, dependencies, "nothing works", test suite failures... +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +What's the problem? + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +What did you expected to happen? + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System (please complete the following information):** + - OS: + - Python version: + - OCRmyPDF version: + +**Installation** +How did you install OCRmyPDF? Did you install it from your operating system's +package manager, or using pip? + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/2-problem-with-a-specific-input-file.md b/.github/ISSUE_TEMPLATE/2-problem-with-a-specific-input-file.md new file mode 100644 index 00000000..595d9cee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-problem-with-a-specific-input-file.md @@ -0,0 +1,40 @@ +--- +name: Problem with a specific input file +about: Something went wrong while trying to OCR a specific file +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +What command line or API call were you trying to run? + +```bash +ocrmypdf ...arguments... input.pdf output.pdf +``` + +Run with verbosity or higher `-v1` to see more detailed logging. This information may be helpful. + +**Example file** +If your issue is a problem that affects only certain files, and we will require an input file (PDF or image) that demonstrates your issue. + +Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/jbarlow83/OCRmyPDF/wiki) for OCRmyPDF's author only. + +Links to files hosted elsewhere are perfectly acceptable. You could also look in ``tests/resources`` and see if any of those files reproduce your issue. + +*(Issues without example files usually cannot be resolved. It's like reporting an issue against a web browser without providing a URL.)* + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System** + - OS: [e.g. Linux, Windows, macOS] + - OCRmyPDF Version: ``ocrmypdf --version`` + - How did you install ocrmypdf? Did you use a system package manager, `pip`, or a Docker image? diff --git a/.github/ISSUE_TEMPLATE/3-feature_request.md b/.github/ISSUE_TEMPLATE/3-feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index f3142596..00000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,33 +0,0 @@ -**Describe the issue** -A clear and concise description of what the issue is. - -**To Reproduce** -What command line were you trying to run? - -```bash -ocrmypdf ...arguments... input.pdf output.pdf -``` - -**Example file** -Please include an example *input* PDF (or image). The input file is more helpful. - -Please check any or all that apply about the test file: - -- [ ] This is the input file -- [ ] The file contains no personal or confidential information -- [ ] I am the copyright holder for this file -- [ ] I permit this file to be included in the OCRmyPDF test suite under the CC-BY-SA 4.0 license -- [ ] I am not the copyright holder, but this file is available under a free software license - -Files that are not free for inclusion in this project are quite welcome, but we like to collect free files for our test suite when possible. Please do *not* submit files with confidential information. At your option you may encrypt files for OCRmyPDF's author only. - -**Expected behavior** -A clear and concise description of what you expected to happen. Include screenshots if applicable. - -**System:** - -- OS: [e.g. Linux, macOS] -- OCRmyPDF Version: [e.g. v7.4.0] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..cad70f3f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,275 @@ +name: Test and deploy + +on: + push: + branches: + - master + - ci + - release/* + tags: + - v* + paths-ignore: + - README* + pull_request: + +jobs: + test_linux: + name: Test ${{ matrix.os }} with Python ${{ matrix.python }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] #, ubuntu-20.04] + python: ["3.6"] #, "3.7", "3.8", "3.9"] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: ${{ matrix.python }} + + - name: Install common packages + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + curl \ + ghostscript \ + img2pdf \ + libffi-dev \ + liblept5 \ + libsm6 libxext6 libxrender-dev \ + pngquant \ + poppler-utils \ + tesseract-ocr \ + tesseract-ocr-deu \ + tesseract-ocr-eng \ + unpaper \ + zlib1g + + - name: Install Ubuntu 18.04 packages + if: matrix.os == 'ubuntu-18.04' + run: | + sudo apt-get install -y --no-install-recommends \ + libexempi3 + + - name: Install Ubuntu 20.04 packages + if: matrix.os == 'ubuntu-20.04' + run: | + sudo apt-get install -y --no-install-recommends \ + libexempi8 + + - name: Install Python packages + run: | + python -m pip install -r requirements/main.txt -r requirements/test.txt . + + - name: Report versions + run: | + tesseract --version + gs --version + pngquant --version + unpaper --version + img2pdf --version + + - name: Test + run: | + python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + files: ./coverage.xml + env_vars: OS,PYTHON + + test_macos: + name: Test macOS + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest] + python: ["3.9"] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: ${{ matrix.python }} + + - name: Install Homebrew deps + run: | + brew update + brew install \ + exempi \ + ghostscript \ + jbig2enc \ + leptonica \ + openjpeg \ + pngquant \ + tesseract + + - name: Install Python packages + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements/main.txt -r requirements/test.txt . + + - name: Report versions + run: | + tesseract --version + gs --version + pngquant --version + img2pdf --version + + - name: Test + run: | + python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + files: ./coverage.xml + env_vars: OS,PYTHON + + test_windows: + name: Test Windows + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + python: ["3.9"] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: ${{ matrix.python }} + + - name: Install system packages + run: | + choco install --yes --no-progress --pre tesseract + choco install --yes --no-progress ghostscript + choco install --yes --no-progress pngquant + + - name: Install Python packages + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements/main.txt -r requirements/test.txt . + + - name: Test + run: | + python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + files: ./coverage.xml + env_vars: OS,PYTHON + + wheel_sdist_linux: + name: Build sdist and wheels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: "3.6" + + - name: Make wheels and sdist + run: | + python -m pip install --upgrade pip wheel + python setup.py sdist + python setup.py bdist_wheel + + - uses: actions/upload-artifact@v2 + with: + path: | + ./dist/*.whl + ./dist/*.tar.gz + + upload_pypi: + name: Deploy artifacts to PyPI + 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') + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.TOKEN_PYPI }} + # repository_url: https://test.pypi.org/legacy/ + + docker: + name: Build Docker images + needs: [wheel_sdist_linux, test_linux, test_macos, test_windows] + runs-on: ubuntu-latest + steps: + - name: Set image tag to release or branch + run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: If master, set to latest + run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV + if: env.DOCKER_IMAGE_TAG == 'master' + + - name: Set Docker Hub repository to username + run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV + + - name: Set image name + run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + with: + fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: jbarlow83 + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - 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/arm64/v8,linux/amd64 \ + --tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \ + --file .docker/Dockerfile . diff --git a/.gitignore b/.gitignore index b2881fb4..60de406b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,44 @@ -# Development environment -.bash_history -.pylintrc -.pytest_cache/ -.ruffus_history.sqlite -.venv/ -*.pyc -*.sublime-* +# dotfiles +.* +!.coveragerc +!.dockerignore +!.git_archival.txt +!.gitattributes +!.gitignore +!.pre-commit-config.yaml +!.readthedocs.yml +!.github/ + +# Dev scratch +*.ipynb +**/*.pyc +/*.pdf +/*.qdf +/*.png +/scratch.py +IDEAS +log/ +tests/resources/private/ +tmp/ +venv*/ +/debug_tests.py +*.traineddata +/private +/coverage.xml # Package building -.eggs/ *.egg-info/ build/ dist/ wheelhouse/ pip-wheel-metadata/ +# Code coverage +htmlcov/ + # Automatically generated files docs/_build/ docs/_static/ docs/_templates/ docs/Makefile ocrmypdf/lib/_*.py - -# Code coverage -.coverage* -htmlcov/ - -# Testing -.ipynb_checkpoints/ -.vscode/ -*.ipynb -*.profile -/*.pdf -/*.qdf -/*.png -/scratch.py -IDEAS -log/ -tests/output/ -tests/resources/private/ -tmp/ -/debug_tests.py -*.traineddata diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b268b628..3cd240e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,23 @@ repos: -- repo: https://github.com/ambv/black - rev: stable + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 hooks: - - id: black - language_version: python3.7 + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - repo: https://github.com/asottile/seed-isort-config + rev: v2.2.0 + hooks: + - id: seed-isort-config + - repo: https://github.com/pre-commit/mirrors-isort + rev: v5.7.0 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + language_version: python + exclude: ^src/ocrmypdf/lib/_leptonica.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7ca3206e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,146 +0,0 @@ -cache: - pip: true - directories: - - $HOME/Library/Caches/Homebrew - -matrix: - include: - - os: linux - dist: trusty - sudo: required - language: python - python: "3.6" - env: - - DIST=trusty - addons: &trusty_apt - apt: - update: true - sources: - - sourceline: 'ppa:alex-p/tesseract-ocr' - - sourceline: 'ppa:heyarje/libav-11' - - sourceline: 'ppa:vshn/ghostscript' - packages: - - ghostscript - - libavcodec56 - - libavformat56 - - libavutil54 - - libexempi3 - - libffi-dev - - pngquant - - poppler-utils - - qpdf - - tesseract-ocr - - tesseract-ocr-deu - - tesseract-ocr-eng - - tesseract-ocr-fra - - os: linux - dist: xenial - sudo: required - language: python - python: "3.7" - env: - - DIST=xenial - addons: - apt: - update: true - sources: - - sourceline: 'ppa:alex-p/tesseract-ocr' - packages: - - ghostscript - - libexempi3 - - libffi-dev - - pngquant - - poppler-utils - - qpdf - - tesseract-ocr - - tesseract-ocr-deu - - tesseract-ocr-eng - - tesseract-ocr-fra - - unpaper - - os: osx - osx_image: xcode9.2 - language: generic - addons: - homebrew: - update: true - packages: - - exempi - - ghostscript - - jbig2enc - - leptonica - - openjpeg - - pngquant - - python - - qpdf - - tesseract - - unpaper - - os: osx - osx_image: xcode9.2 - language: generic - env: - - ADD_PDFMINER=1 - addons: - homebrew: - update: true - packages: - - exempi - - ghostscript - - jbig2enc - - leptonica - - openjpeg - - pngquant - - python - - qpdf - - tesseract - - unpaper - -before_cache: -- rm -f $HOME/.cache/pip/log/debug.log - -before_install: | - mkdir -p bin - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - pip3 install --upgrade pip - pip3 install --upgrade wheel - if [[ "$DIST" == "trusty" ]]; then - mkdir -p packages - wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb - sudo dpkg -i packages/unpaper_6.1-1.deb - fi - elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - pip3 install --upgrade pip - pip3 install wheel - fi - -install: -- export PATH=$PWD/bin:$PATH -- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251 -- pip3 install -r requirements/main.txt -- pip3 install --no-deps . -- | - if [[ "$ADD_PDFMINER" == "1" ]]; then - pip3 install --no-deps .[pdfminer] - fi -- pip3 install -r requirements/test.txt - -script: -- tesseract --version -- qpdf --version -- pytest -n auto - -deploy: - # release for main pypi - # 3.6 is considered the build leader and does the deploy, otherwise there is - # a race and all versions will try to deploy - # OTOH if we ever need separate binary wheels then each version needs its - # own deploy -- provider: pypi - user: ocrmypdf-travis - password: - secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo=" - distributions: "sdist bdist_wheel" - on: - branch: master - tags: true - condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux" - skip_upload_docs: true diff --git a/LICENSE b/LICENSE index f288702d..a612ad98 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,373 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2021f662..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,43 +0,0 @@ -# requirements -recursive-include requirements * - -# git -include .git_archival.txt - -# docker -include .dockerignore -recursive-include .docker * - -# tests -include .coveragerc -recursive-include tests *.bin -recursive-include tests *.jpg -recursive-include tests *.jsonl -recursive-include tests *.png -recursive-include tests *.pdf -recursive-include tests *.py -recursive-include tests *.rst -recursive-include tests *.txt -recursive-exclude tests/resources/private * - -# documentation -include LICENSE -include *.rst -recursive-exclude .github * -recursive-include docs *.py -recursive-include docs *.rst -recursive-include docs *.svg -recursive-exclude docs/_build * - - -# support files -recursive-include src/ocrmypdf/data * -include *.py -exclude tasks.py -recursive-exclude .travis * -exclude .travis* - - -# code -exclude src/ocrmypdf/lib/_leptonica.py -exclude scratch.py diff --git a/README.md b/README.md index 3509fb71..a0bf0a35 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ -OCRmyPDF -======== +OCRmyPDF -[![Travis build status][travis]](https://travis-ci.org/jbarlow83/OCRmyPDF) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] +[![Build Status](https://github.com/jbarlow83/OCRmyPDF/actions/workflows/build.yml/badge.svg)](https://github.com/jbarlow83/OCRmyPDF/actions/workflows/build.yml) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions] +[azure]: https://dev.azure.com/jim0585/ocrmypdf/_apis/build/status/jbarlow83.OCRmyPDF?branchName=master [travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status" - [pypi]: https://img.shields.io/pypi/v/ocrmypdf.svg "PyPI version" - [homebrew]: https://img.shields.io/homebrew/v/ocrmypdf.svg "Homebrew version" - [docs]: https://readthedocs.org/projects/ocrmypdf/badge/?version=latest "RTD" +[pyversions]: https://img.shields.io/pypi/pyversions/ocrmypdf "Supported Python versions" OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted. @@ -27,15 +25,14 @@ ocrmypdf # it's a scriptable command line program [See the release notes for details on the latest changes](https://ocrmypdf.readthedocs.io/en/latest/release_notes.html). -Main features -------------- +## Main features - Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF - Places OCR text accurately below the image to ease copy / paste - Keeps the exact resolution of the original embedded images - When possible, inserts OCR information as a "lossless" operation without disrupting any other content - Optimizes PDF images, often producing files smaller than the input file -- If requested deskews and/or cleans the image before performing OCR +- If requested, deskews and/or cleans the image before performing OCR - Validates input and output files - 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) @@ -44,10 +41,9 @@ Main features For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/). -Motivation ----------- +## Motivation -I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: I found many, but none of them were really satisfying. +I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying: - Either they produced PDF files with misplaced text under the image (making copy/paste impossible) - Or they did not handle accents and multilingual characters @@ -59,33 +55,23 @@ I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: ...so I decided to develop my own tool. -Installation ------------- +## Installation -Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows. +Linux, Windows, macOS and FreeBSD are supported. Docker images are also available. -Users of Debian 9 or later or Ubuntu 16.10 or later may simply - -```bash -apt-get install ocrmypdf -``` - -and users of Fedora 29 or later may simply - -```bash -dnf install ocrmypdf -``` - -and macOS users with Homebrew may simply - -```bash -brew install ocrmypdf -``` +| Operating system | Install command | +| ----------------------------- | ------------------------------| +| Debian, Ubuntu | ``apt install ocrmypdf`` | +| Windows Subsystem for Linux | ``apt install ocrmypdf`` | +| Fedora | ``dnf install ocrmypdf`` | +| macOS | ``brew install ocrmypdf`` | +| LinuxBrew | ``brew install ocrmypdf`` | +| FreeBSD | ``pkg install py37-ocrmypdf`` | +| Conda | ``conda install ocrmypdf`` | For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps. -Languages ---------- +## Languages OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users, you can often find packages that provide language packs: @@ -94,15 +80,20 @@ OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux use apt-cache search tesseract-ocr # Debian/Ubuntu users -apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back +apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language pack + +# Arch Linux users +pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs + +# brew macOS users +brew install tesseract-lang ``` You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested. -Documentation and support -------------------------- +## Documentation and support -Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via: +Once OCRmyPDF is installed, the built-in help which explains the command syntax and options can be accessed via: ```bash ocrmypdf --help @@ -110,42 +101,37 @@ ocrmypdf --help Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html). -If you detect an issue, please: +Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPDF/issues) page, and follow the issue template for quick response. -- Check whether your issue is already known -- If no problem report exists on github, please create one here: -- Describe your problem thoroughly -- Append the console output of the script when running the debug mode (`-v 1` option) -- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox) +## Requirements -Requirements ------------- +In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. OCRmyPDF is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD. -Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings. - -Press & Media -------------- +## Press & Media - [Going paperless with OCRmyPDF](https://medium.com/@ikirichenko/going-paperless-with-ocrmypdf-e2f36143f46a) - [Converting a scanned document into a compressed searchable PDF with redactions](https://medium.com/@treyharris/converting-a-scanned-document-into-a-compressed-searchable-pdf-with-redactions-63f61c34fe4c) -- [c't 1-2014, page 59](http://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't -- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](http://heise.de/-2356670) +- [c't 1-2014, page 59](https://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't +- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](https://heise.de/-2356670) +- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html) +- [Excellent Utilities: OCRmyPDF](https://www.linuxlinks.com/excellent-utilities-ocrmypdf-add-ocr-text-layer-scanned-pdfs/) -Business enquiries ------------------- +## Business enquiries -OCRmyPDF would not be the software that it is today is without companies and users choosing to provide support for feature development and consulting enquiries. We are happy to discuss all enquiries, whether for extending the existing feature set, or integrating OCRmyPDF into a larger system. +OCRmyPDF would not be the software that it is today without companies and users choosing to provide support for feature development and consulting enquiries. We are happy to discuss all enquiries, whether for extending the existing feature set, or integrating OCRmyPDF into a larger system. -License -------- +## License -The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files. +The OCRmyPDF software is licensed under the Mozilla Public License 2.0 +(MPL-2.0). This license permits integration of OCRmyPDF with other code, +included commercial and closed source, but asks you to publish source-level +modifications you make to OCRmyPDF. -The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0). +Some components of OCRmyPDF have other licenses, as noted in those files and the +``debian/copyright`` file. Most files in ``misc/`` use the MIT license, and the +documentation and test files are generally licensed under Creative Commons +ShareAlike 4.0 (CC-BY-SA 4.0). -OCRmyPDF versions prior to 6.0 were distributed under the MIT License. - -Disclaimer ----------- +## Disclaimer The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/debian/copyright b/debian/copyright index 8c190b4d..a48ac1dc 100644 --- a/debian/copyright +++ b/debian/copyright @@ -2,35 +2,70 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: OCRmyPDF Upstream-Contact: James R. Barlow Source: https://github.com/jbarlow83/OCRmyPDF -Files-Excluded: tests/resources/milk.pdf Files: * Copyright: - (C) 2013-2017 The OCRmyPDF Authors - (C) 2013-2016, 2015-2017 2016, 2017, 2017-2018, 2018 James R. Barlow -License: GPL-3+ + (C) 2013-2015 Julien Pfefferkorn + (C) 2015-2020 James R. Barlow + (C) 2019 Martin Wind +License: MPL-2.0 + +Files: misc/* +Copyright: + (C) 2020 James R. Barlow +License: Expat + +Files: misc/completion/ocrmypdf.bash +Copyright: + (C) 2019 Frank Pille + (C) 2020 Alex Willner +License: Expat + +Files: misc/completion/ocrmypdf.fish +Copyright: + (C) 2020 James R. Barlow +License: Expat + +Files: misc/batch.py +Copyright: + (C) 2016 findingorder: https://github.com/findingorder +License: Expat + +Files: misc/synology.py +Copyright: + (C) github.com/Enantiomerie +License: Expat + +Files: misc/watcher.py +Copyright: + (C) 2019 Ian Alexander: https://github.com/ianalexander + (C) 2020 James R. Barlow +License: Expat + +Files: misc/webservice.py +Copyright: (C) 2019 James R. Barlow +License: AGPL-3+ Files: docs tests/resources/* Copyright: (C) 2013-2018 James R. Barlow License: CC-BY-SA-4.0 +Files: docs/images/bitmap_vs_svg.svg +Copyright: (C) 2006 Yug +License: CC-BY-SA-2.5 + Files: src/ocrmypdf/hocrtransform.py Copyright: (C) 2010 Jonathan Brinley (C) 2013-14 Julien Pfefferkorn (C) 2015-16 James R. Barlow License: Expat -Files: src/ocrmypdf/pdfa.py -Copyright: (C) 2015 James R. Barlow - (C) 1986-2017 The authors of GhostScript -License: GPL-3+ - Files: src/ocrmypdf/_unicodefun.py Copyright: (C) 2014 Armin Ronacher (C) 2017 James R. Barlow License: BSD-3-clause -Files: tests/spoof/* +Files: tests/plugins/* Copyright: (C) 2016, 2017, 2016-2018 James R. Barlow License: Expat @@ -82,12 +117,17 @@ License: CC-BY-SA-3.0 Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf Copyright: (C) 2005 Ellywa License: GFDL-1.2+ or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0 +Comment: + Obtained from: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif Files: tests/resources/overlay.pdf Copyright: (C) 2017 Max Anderson License: Expat -Files: tests/resources/baiona*.png +Files: + tests/resources/baiona*.png + tests/resources/baiona*.jpg + tests/resources/link.pdf Copyright: (C) 2014 Euskaldunaa License: CC-BY-SA-4.0 @@ -95,11 +135,12 @@ Files: tests/resources/vector.pdf Copyright: (C) 2018 Catscratch License: Expat -Files: test/resources/enron*.pdf -Copyright: EnronData.org -License: CC-BY-3.0 - See: https://enrondata.readthedocs.io/en/latest/data/edo-enron-email-pst-dataset/ -Comment: Unprocessed. +Files: tests/resources/3small.pdf +Copyright: (C) 2014 Euskaldunaa + (C) 2017 James R. Barlow + (C) 2005 Ellywa +License: CC-BY-SA-4.0 and (GFDL-1.2+ or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0) +Comment: concatenation of baiona_gray.png, crom.png and typewriter.png/2400dpi.pdf Files: src/ocrmypdf/data/sRGB.icc Copyright: Kai-Uwe Behrmann @@ -113,6 +154,13 @@ Files: debian/* Copyright: (C) 2016 Sean Whitton License: GPL-3+ +License: MPL-2.0 + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. + . + On Debian systems the full text of the MPL-2.0 can be found in + /usr/share/common-licenses/MPL-2.0. + License: GPL-3+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -130,6 +178,669 @@ License: GPL-3+ On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". +License: AGPL-3+ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + . + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + . + Preamble + . + The GNU Affero General Public License is a free, copyleft license for + software and other kinds of works, specifically designed to ensure + cooperation with the community in the case of network server software. + . + The licenses for most software and other practical works are designed + to take away your freedom to share and change the works. By contrast, + our General Public Licenses are intended to guarantee your freedom to + share and change all versions of a program--to make sure it remains free + software for all its users. + . + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + them if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs, and that you know you can do these things. + . + Developers that use our General Public Licenses protect your rights + with two steps: (1) assert copyright on the software, and (2) offer + you this License which gives you legal permission to copy, distribute + and/or modify the software. + . + A secondary benefit of defending all users' freedom is that + improvements made in alternate versions of the program, if they + receive widespread use, become available for other developers to + incorporate. Many developers of free software are heartened and + encouraged by the resulting cooperation. However, in the case of + software used on network servers, this result may fail to come about. + The GNU General Public License permits making a modified version and + letting the public access it on a server without ever releasing its + source code to the public. + . + The GNU Affero General Public License is designed specifically to + ensure that, in such cases, the modified source code becomes available + to the community. It requires the operator of a network server to + provide the source code of the modified version running there to the + users of that server. Therefore, public use of a modified version, on + a publicly accessible server, gives the public access to the source + code of the modified version. + . + An older license, called the Affero General Public License and + published by Affero, was designed to accomplish similar goals. This is + a different license, not a version of the Affero GPL, but Affero has + released a new version of the Affero GPL which permits relicensing under + this license. + . + The precise terms and conditions for copying, distribution and + modification follow. + . + TERMS AND CONDITIONS + . + 0. Definitions. + . + "This License" refers to version 3 of the GNU Affero General Public License. + . + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + . + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + . + To "modify" a work means to copy from or adapt all or part of the work + in a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + . + A "covered work" means either the unmodified Program or a work based + on the Program. + . + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + . + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through + a computer network, with no transfer of a copy, is not conveying. + . + An interactive user interface displays "Appropriate Legal Notices" + to the extent that it includes a convenient and prominently visible + feature that (1) displays an appropriate copyright notice, and (2) + tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the + work under this License, and how to view a copy of this License. If + the interface presents a list of user commands or options, such as a + menu, a prominent item in the list meets this criterion. + . + 1. Source Code. + . + The "source code" for a work means the preferred form of the work + for making modifications to it. "Object code" means any non-source + form of a work. + . + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that + is widely used among developers working in that language. + . + The "System Libraries" of an executable work include anything, other + than the work as a whole, that (a) is included in the normal form of + packaging a Major Component, but which is not part of that Major + Component, and (b) serves only to enable use of the work with that + Major Component, or to implement a Standard Interface for which an + implementation is available to the public in source code form. A + "Major Component", in this context, means a major essential component + (kernel, window system, and so on) of the specific operating system + (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. + . + The "Corresponding Source" for a work in object code form means all + the source code needed to generate, install, and (for an executable + work) run the object code and to modify the work, including scripts to + control those activities. However, it does not include the work's + System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but + which are not part of the work. For example, Corresponding Source + includes interface definition files associated with source files for + the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, + such as by intimate data communication or control flow between those + subprograms and other parts of the work. + . + The Corresponding Source need not include anything that users + can regenerate automatically from other parts of the Corresponding + Source. + . + The Corresponding Source for a work in source code form is that + same work. + . + 2. Basic Permissions. + . + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program. The output from running a + covered work is covered by this License only if the output, given its + content, constitutes a covered work. This License acknowledges your + rights of fair use or other equivalent, as provided by copyright law. + . + You may make, run and propagate covered works that you do not + convey, without conditions so long as your license otherwise remains + in force. You may convey covered works to others for the sole purpose + of having them make modifications exclusively for you, or provide you + with facilities for running those works, provided that you comply with + the terms of this License in conveying all material for which you do + not control copyright. Those thus making or running the covered works + for you must do so exclusively on your behalf, under your direction + and control, on terms that prohibit them from making any copies of + your copyrighted material outside their relationship with you. + . + Conveying under any other circumstances is permitted solely under + the conditions stated below. Sublicensing is not allowed; section 10 + makes it unnecessary. + . + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + . + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article + 11 of the WIPO copyright treaty adopted on 20 December 1996, or + similar laws prohibiting or restricting circumvention of such + measures. + . + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention + is effected by exercising rights under this License with respect to + the covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of + technological measures. + . + 4. Conveying Verbatim Copies. + . + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; + keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; + keep intact all notices of the absence of any warranty; and give all + recipients a copy of this License along with the Program. + . + You may charge any price or no price for each copy that you convey, + and you may offer support or warranty protection for a fee. + . + 5. Conveying Modified Source Versions. + . + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the + terms of section 4, provided that you also meet all of these conditions: + . + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + . + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + . + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + . + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + . + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, + and which are not combined with it such as to form a larger program, + in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not + used to limit the access or legal rights of the compilation's users + beyond what the individual works permit. Inclusion of a covered work + in an aggregate does not cause this License to apply to the other + parts of the aggregate. + . + 6. Conveying Non-Source Forms. + . + You may convey a covered work in object code form under the terms + of sections 4 and 5, provided that you also convey the + machine-readable Corresponding Source under the terms of this License, + in one of these ways: + . + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + . + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + . + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + . + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + . + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + . + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be + included in conveying the object code work. + . + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + . + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + . + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as + part of a transaction in which the right of possession and use of the + User Product is transferred to the recipient in perpetuity or for a + fixed term (regardless of how the transaction is characterized), the + Corresponding Source conveyed under this section must be accompanied + by the Installation Information. But this requirement does not apply + if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has + been installed in ROM). + . + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access to a + network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and + protocols for communication across the network. + . + Corresponding Source conveyed, and Installation Information provided, + in accord with this section must be in a format that is publicly + documented (and with an implementation available to the public in + source code form), and must require no special password or key for + unpacking, reading or copying. + . + 7. Additional Terms. + . + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall + be treated as though they were included in this License, to the extent + that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately + under those permissions, but the entire Program remains governed by + this License without regard to the additional permissions. + . + When you convey a copy of a covered work, you may at your option + remove any additional permissions from that copy, or from any part of + it. (Additional permissions may be written to require their own + removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, + for which you have or can give appropriate copyright permission. + . + Notwithstanding any other provision of this License, for material you + add to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + . + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + . + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + . + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + . + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + . + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + . + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + . + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further + restriction, you may remove that term. If a license document contains + a further restriction but permits relicensing or conveying under this + License, you may add to a covered work material governed by the terms + of that license document, provided that the further restriction does + not survive such relicensing or conveying. + . + If you add terms to a covered work in accord with this section, you + must place, in the relevant source files, a statement of the + additional terms that apply to those files, or a notice indicating + where to find the applicable terms. + . + Additional terms, permissive or non-permissive, may be stated in the + form of a separately written license, or stated as exceptions; + the above requirements apply either way. + . + 8. Termination. + . + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or + modify it is void, and will automatically terminate your rights under + this License (including any patent licenses granted under the third + paragraph of section 11). + . + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and (b) permanently, if the copyright + holder fails to notify you of the violation by some reasonable means + prior to 60 days after the cessation. + . + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you have + received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after + your receipt of the notice. + . + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + . + 9. Acceptance Not Required for Having Copies. + . + You are not required to accept this License in order to receive or + run a copy of the Program. Ancillary propagation of a covered work + occurring solely as a consequence of using peer-to-peer transmission + to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or + modify any covered work. These actions infringe copyright if you do + not accept this License. Therefore, by modifying or propagating a + covered work, you indicate your acceptance of this License to do so. + . + 10. Automatic Licensing of Downstream Recipients. + . + Each time you convey a covered work, the recipient automatically + receives a license from the original licensors, to run, modify and + propagate that work, subject to this License. You are not responsible + for enforcing compliance by third parties with this License. + . + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered + work results from an entity transaction, each party to that + transaction who receives a copy of the work also receives whatever + licenses to the work the party's predecessor in interest had or could + give under the previous paragraph, plus a right to possession of the + Corresponding Source of the work from the predecessor in interest, if + the predecessor has it or can get it with reasonable efforts. + . + You may not impose any further restrictions on the exercise of the + rights granted or affirmed under this License. For example, you may + not impose a license fee, royalty, or other charge for exercise of + rights granted under this License, and you may not initiate litigation + (including a cross-claim or counterclaim in a lawsuit) alleging that + any patent claim is infringed by making, using, selling, offering for + sale, or importing the Program or any portion of it. + . + 11. Patents. + . + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The + work thus licensed is called the contributor's "contributor version". + . + A contributor's "essential patent claims" are all patent claims + owned or controlled by the contributor, whether already acquired or + hereafter acquired, that would be infringed by some manner, permitted + by this License, of making, using, or selling its contributor version, + but do not include claims that would be infringed only as a + consequence of further modification of the contributor version. For + purposes of this definition, "control" includes the right to grant + patent sublicenses in a manner consistent with the requirements of + this License. + . + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to + make, use, sell, offer for sale, import and otherwise run, modify and + propagate the contents of its contributor version. + . + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a + party means to make such an agreement or commitment not to enforce a + patent against the party. + . + If you convey a covered work, knowingly relying on a patent license, + and the Corresponding Source of the work is not available for anyone + to copy, free of charge and under the terms of this License, through a + publicly available network server or other readily accessible means, + then you must either (1) cause the Corresponding Source to be so + available, or (2) arrange to deprive yourself of the benefit of the + patent license for this particular work, or (3) arrange, in a manner + consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have + actual knowledge that, but for the patent license, your conveying the + covered work in a country, or your recipient's use of the covered work + in a country, would infringe one or more identifiable patents in that + country that you have reason to believe are valid. + . + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties + receiving the covered work authorizing them to use, propagate, modify + or convey a specific copy of the covered work, then the patent license + you grant is automatically extended to all recipients of the covered + work and works based on it. + . + A patent license is "discriminatory" if it does not include within + the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are + specifically granted under this License. You may not convey a covered + work if you are a party to an arrangement with a third party that is + in the business of distributing software, under which you make payment + to the third party based on the extent of your activity of conveying + the work, and under which the third party grants, to any of the + parties who would receive the covered work from you, a discriminatory + patent license (a) in connection with copies of the covered work + conveyed by you (or copies made from those copies), or (b) primarily + for and in connection with specific products or compilations that + contain the covered work, unless you entered into that arrangement, + or that patent license was granted, prior to 28 March 2007. + . + Nothing in this License shall be construed as excluding or limiting + any implied license or other defenses to infringement that may + otherwise be available to you under applicable patent law. + . + 12. No Surrender of Others' Freedom. + . + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot convey a + covered work so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you + to collect a royalty for further conveying from those to whom you convey + the Program, the only way you could satisfy both those terms and this + License would be to refrain entirely from conveying the Program. + . + 13. Remote Network Interaction; Use with the GNU General Public License. + . + Notwithstanding any other provision of this License, if you modify the + Program, your modified version must prominently offer all users + interacting with it remotely through a computer network (if your version + supports such interaction) an opportunity to receive the Corresponding + Source of your version by providing access to the Corresponding Source + from a network server at no charge, through some standard or customary + means of facilitating copying of software. This Corresponding Source + shall include the Corresponding Source for any work covered by version 3 + of the GNU General Public License that is incorporated pursuant to the + following paragraph. + . + Notwithstanding any other provision of this License, you have + permission to link or combine any covered work with a work licensed + under version 3 of the GNU General Public License into a single + combined work, and to convey the resulting work. The terms of this + License will continue to apply to the part which is the covered work, + but the work with which it is combined will remain governed by version + 3 of the GNU General Public License. + . + 14. Revised Versions of this License. + . + The Free Software Foundation may publish revised and/or new versions of + the GNU Affero General Public License from time to time. Such new versions + will be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + . + Each version is given a distinguishing version number. If the + Program specifies that a certain numbered version of the GNU Affero General + Public License "or any later version" applies to it, you have the + option of following the terms and conditions either of that numbered + version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the + GNU Affero General Public License, you may choose any version ever published + by the Free Software Foundation. + . + If the Program specifies that a proxy can decide which future + versions of the GNU Affero General Public License can be used, that proxy's + public statement of acceptance of a version permanently authorizes you + to choose that version for the Program. + . + Later license versions may give you additional or different + permissions. However, no additional obligations are imposed on any + author or copyright holder as a result of your choosing to follow a + later version. + . + 15. Disclaimer of Warranty. + . + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + . + 16. Limitation of Liability. + . + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES. + . + 17. Interpretation of Sections 15 and 16. + . + If the disclaimer of warranty and limitation of liability provided + above cannot be given local legal effect according to their terms, + reviewing courts shall apply local law that most closely approximates + an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a + copy of the Program in return for a fee. + . + END OF TERMS AND CONDITIONS + . + How to Apply These Terms to Your New Programs + . + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + . + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + state the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + . + + Copyright (C) + . + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + . + Also add information on how to contact you by electronic and paper mail. + . + If your software can interact with users remotely through a computer + network, you should also make sure that it provides a way for users to + get its source. For example, if your program is a web application, its + interface could display a "Source" link that leads users to an archive + of the code. There are many ways you could offer source, and different + solutions will be better for different programs; see section 13 for the + specific requirements. + . + You should also get your employer (if you work as a programmer) or school, + if any, to sign a "copyright disclaimer" for the program, if necessary. + For more information on this, and how to apply and follow the GNU AGPL, see + . + License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/docs/advanced.rst b/docs/advanced.rst index da1f1675..09a7376a 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -1,16 +1,34 @@ +================= Advanced features ================= Control of unpaper ------------------- +================== -OCRmyPDF uses ``unpaper`` to provide the implementation of the ``--clean`` and ``--clean-final`` arguments. `unpaper `_ provides a variety of image processing filters to improve images. +OCRmyPDF uses ``unpaper`` to provide the implementation of the +``--clean`` and ``--clean-final`` arguments. +`unpaper `__ +provides a variety of image processing filters to improve images. -By default, OCRmyPDF uses only ``unpaper`` arguments that were found to be safe to use on almost all files without having to inspect every page of the file afterwards. This is particularly true when only ``--clean`` is used, since that instructs OCRmyPDF to only clean the image before OCR and not the final image. +By default, OCRmyPDF uses only ``unpaper`` arguments that were found to +be safe to use on almost all files without having to inspect every page +of the file afterwards. This is particularly true when only ``--clean`` +is used, since that instructs OCRmyPDF to only clean the image before +OCR and not the final image. -However, if you wish to use the more aggressive options in ``unpaper``, you may use ``--unpaper-args '...'`` to override the OCRmyPDF's defaults and forward other arguments to unpaper. This option will forward arguments to ``unpaper`` without any knowledge of what that program considers to be valid arguments. The string of arguments must be quoted as shown in the examples below. No filename arguments may be included. OCRmyPDF will assume it can append input and output filename of intermediate images to the ``--unpaper-args`` string. +However, if you wish to use the more aggressive options in ``unpaper``, +you may use ``--unpaper-args '...'`` to override the OCRmyPDF's defaults +and forward other arguments to unpaper. This option will forward +arguments to ``unpaper`` without any knowledge of what that program +considers to be valid arguments. The string of arguments must be quoted +as shown in the examples below. No filename arguments may be included. +OCRmyPDF will assume it can append input and output filename of +intermediate images to the ``--unpaper-args`` string. -In this example, we tell ``unpaper`` to expect two pages of text on a sheet (image), such as occurs when two facing pages of a book are scanned. ``unpaper`` uses this information to deskew each independently and clean up the margins of both. +In this example, we tell ``unpaper`` to expect two pages of text on a +sheet (image), such as occurs when two facing pages of a book are +scanned. ``unpaper`` uses this information to deskew each independently +and clean up the margins of both. .. code-block:: bash @@ -19,40 +37,71 @@ In this example, we tell ``unpaper`` to expect two pages of text on a sheet (ima .. warning:: - Some ``unpaper`` features will reposition text within the image. ``--clean-final`` is recommended to avoid this issue. + Some ``unpaper`` features will reposition text within the image. + ``--clean-final`` is recommended to avoid this issue. .. warning:: - Some ``unpaper`` features cause multiple input or output files to be consumed or produced. OCRmyPDF requires ``unpaper`` to consume one file and produce one file. An deviation from that condition will result in errors. + Some ``unpaper`` features cause multiple input or output files to be + consumed or produced. OCRmyPDF requires ``unpaper`` to consume one + file and produce one file. An deviation from that condition will + result in errors. .. note:: - ``unpaper`` uses uncompressed PBM/PGM/PPM files for its intermediate files. For large images or documents, it can take a lot of temporary disk space. + ``unpaper`` uses uncompressed PBM/PGM/PPM files for its intermediate + files. For large images or documents, it can take a lot of temporary + disk space. Control of OCR options ----------------------- +====================== -OCRmyPDF provides many features to control the behavior of the OCR engine, Tesseract. +OCRmyPDF provides many features to control the behavior of the OCR +engine, Tesseract. When OCR is skipped -""""""""""""""""""" +------------------- -If a page in a PDF seems to have text, by default OCRmyPDF will exit without modifying the PDF. This is to ensure that PDFs that were previously OCRed or were "born digital" rather than scanned are not processed. +If a page in a PDF seems to have text, by default OCRmyPDF will exit +without modifying the PDF. This is to ensure that PDFs that were +previously OCRed or were "born digital" rather than scanned are not +processed. -If ``--skip-text`` is issued, then no OCR will be performed on pages that already have text. The page will be copied to the output. This may be useful for documents that contain both "born digital" and scanned content, or to use OCRmyPDF to normalize and convert to PDF/A regardless of their contents. +If ``--skip-text`` is issued, then no OCR will be performed on pages +that already have text. The page will be copied to the output. This may +be useful for documents that contain both "born digital" and scanned +content, or to use OCRmyPDF to normalize and convert to PDF/A regardless +of their contents. -If ``--redo-ocr`` is issued, then a detailed text analysis is performed. Text is categorized as either visible or invisible. Invisible text (OCR) 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 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 disrupting the existing text. +If ``--redo-ocr`` is issued, then a detailed text analysis is performed. +Text is categorized as either visible or invisible. Invisible text (OCR) +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 +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 +disrupting the existing text. -If ``--force-ocr`` is issued, then all pages will be rasterized to images, discarding any hidden OCR text, and rasterizing any printable text. This is useful for redoing OCR, for fixing OCR text with a damaged character map (text is selectable but not searchable), and destroying redacted information. Any forms and vector graphics will be rasterized as well. +If ``--force-ocr`` is issued, then all pages will be rasterized to +images, discarding any hidden OCR text, and rasterizing any printable +text. This is useful for redoing OCR, for fixing OCR text with a damaged +character map (text is selectable but not searchable), and destroying +redacted information. Any forms and vector graphics will be rasterized +as well. Time and image size limits -"""""""""""""""""""""""""" +-------------------------- -By default, OCRmyPDF permits tesseract to run for three minutes (180 seconds) per page. This is usually more than enough time to find all text on a reasonably sized page with modern hardware. +By default, OCRmyPDF permits tesseract to run for three minutes (180 +seconds) per page. This is usually more than enough time to find all +text on a reasonably sized page with modern hardware. -If a page is skipped, it will be inserted without OCR. If preprocessing was requested, the preprocessed image layer will be inserted. +If a page is skipped, it will be inserted without OCR. If preprocessing +was requested, the preprocessed image layer will be inserted. -If you want to adjust the amount of time spent on OCR, change ``--tesseract-timeout``. You can also automatically skip images that exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI, 8.5×11" page is 8.4 megapixels.) +If you want to adjust the amount of time spent on OCR, change +``--tesseract-timeout``. You can also automatically skip images that +exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI, +8.5×11" page is 8.4 megapixels.) .. code-block:: bash @@ -60,21 +109,26 @@ If you want to adjust the amount of time spent on OCR, change ``--tesseract-time ocrmypdf --tesseract-timeout 300 --skip-big 50 bigfile.pdf output.pdf Overriding default tesseract -"""""""""""""""""""""""""""" +---------------------------- OCRmyPDF checks the system ``PATH`` for the ``tesseract`` binary. -Some relevant environment variables that influence Tesseract's behavior include: +Some relevant environment variables that influence Tesseract's behavior +include: .. envvar:: TESSDATA_PREFIX - Overrides the path to Tesseract's data files. This can allow simultaneous installation of the "best" and "fast" training data sets. OCRmyPDF does not manage this environment variable. + Overrides the path to Tesseract's data files. This can allow + simultaneous installation of the "best" and "fast" training data + sets. OCRmyPDF does not manage this environment variable. .. envvar:: OMP_THREAD_LIMIT - Controls the number of threads Tesseract will use. OCRmyPDF will manage this environment if it is not already set. (Currently, it will set it to 1 because this gives the best results in testing.) + Controls the number of threads Tesseract will use. OCRmyPDF will + manage this environment variable if it is not already set. -For example, if you have a development build of Tesseract don't wish to use the system installation, you can launch OCRmyPDF as follows: +For example, if you have a development build of Tesseract don't wish to +use the system installation, you can launch OCRmyPDF as follows: .. code-block:: bash @@ -83,26 +137,34 @@ For example, if you have a development build of Tesseract don't wish to use the TESSDATA_PREFIX=/home/user/src/tesseract \ ocrmypdf input.pdf output.pdf -In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to an alternate folder for its "tessdata" files. +In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to +an alternate folder for its "tessdata" files. Overriding other support programs -""""""""""""""""""""""""""""""""" +--------------------------------- In addition to tesseract, OCRmyPDF uses the following external binaries: -* ``gs`` (Ghostscript) -* ``unpaper`` -* ``qpdf`` - -In each case OCRmyPDF will search the ``PATH`` environment variable to locate the binaries. +- ``gs`` (Ghostscript) +- ``unpaper`` +- ``pngquant`` +- ``jbig2`` +In each case OCRmyPDF will search the ``PATH`` environment variable to +locate the binaries. Changing tesseract configuration variables -"""""""""""""""""""""""""""""""""""""""""" +------------------------------------------ -You can override tesseract's default `control parameters `_ with a configuration file. +You can override tesseract's default `control +parameters `__ +with a configuration file. -As an example, this configuration will disable Tesseract's dictionary for current language. Normally the dictionary is helpful for interpolating words that are unclear, but it may interfere with OCR if the document does not contain many words (for example, a list of part numbers). +As an example, this configuration will disable Tesseract's dictionary +for current language. Normally the dictionary is helpful for +interpolating words that are unclear, but it may interfere with OCR if +the document does not contain many words (for example, a list of part +numbers). Create a file named "no-dict.cfg" with these contents: @@ -120,11 +182,11 @@ then run ocrmypdf as follows (along with any other desired arguments): .. warning:: - Some combinations of control parameters will break Tesseract or break assumptions that OCRmyPDF makes about Tesseract's output. - + Some combinations of control parameters will break Tesseract or break + assumptions that OCRmyPDF makes about Tesseract's output. Changing the PDF renderer -------------------------- +========================= rasterizing Converting a PDF to an image for display. @@ -132,42 +194,63 @@ rasterizing rendering Creating a new PDF from other data (such as an existing PDF). - -OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` always selects ``sandwich``. +OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The +renderer may be selected using ``--pdf-renderer``. The default is +``auto`` which lets OCRmyPDF select the renderer to use. Currently, +``auto`` always selects ``sandwich``. The ``sandwich`` renderer -""""""""""""""""""""""""" +------------------------- -The ``sandwich`` renderer uses Tesseract's new text-only PDF feature, which produces a PDF page that lays out the OCR in invisible text. This page is then "sandwiched" onto the original PDF page, allowing lossless application of OCR even to PDF pages that contain other vector objects. +The ``sandwich`` renderer uses Tesseract's new text-only PDF feature, +which produces a PDF page that lays out the OCR in invisible text. This +page is then "sandwiched" onto the original PDF page, allowing lossless +application of OCR even to PDF pages that contain other vector objects. -Currently this is the best renderer for most uses, however it is implemented in Tesseract so OCRmyPDF cannot influence it. Currently some problematic PDF viewers like Mozilla PDF.js and macOS Preview have problems with segmenting its text output, and mightrunseveralwordstogether. +Currently this is the best renderer for most uses, however it is +implemented in Tesseract so OCRmyPDF cannot influence it. Currently some +problematic PDF viewers like Mozilla PDF.js and macOS Preview have +problems with segmenting its text output, and +mightrunseveralwordstogether. -When image preprocessing features like ``--deskew`` are used, the original PDF will be rendered as a full page and the OCR layer will be placed on top. +When image preprocessing features like ``--deskew`` are used, the +original PDF will be rendered as a full page and the OCR layer will be +placed on top. The ``hocr`` renderer -""""""""""""""""""""" +--------------------- -The ``hocr`` renderer works with older versions of Tesseract. The image layer is copied from the original PDF page if possible, avoiding potentially lossy transcoding or loss of other PDF information. If preprocessing is specified, then the image layer is a new PDF. +The ``hocr`` renderer works with older versions of Tesseract. The image +layer is copied from the original PDF page if possible, avoiding +potentially lossy transcoding or loss of other PDF information. If +preprocessing is specified, then the image layer is a new PDF. -Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone looking to customize how OCR is presented should look here. A major disadvantage of this renderer is it not capable of correctly handling text outside the Latin alphabet. Pull requests to improve the situation are welcome. +Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone +looking to customize how OCR is presented should look here. A major +disadvantage of this renderer is it not capable of correctly handling +text outside the Latin alphabet. Pull requests to improve the situation +are welcome. -Currently, this renderer has the best compatibility with Mozilla's PDF.js viewer. +Currently, this renderer has the best compatibility with Mozilla's +PDF.js viewer. This works in all versions of Tesseract. The ``tesseract`` renderer -"""""""""""""""""""""""""" +-------------------------- -The ``tesseract`` renderer was removed. OCRmyPDF's new approach to text layer grafting makes it functionally equivalent to ``sandwich``. +The ``tesseract`` renderer was removed. OCRmyPDF's new approach to text +layer grafting makes it functionally equivalent to ``sandwich``. Return code policy ------------------- +================== -OCRmyPDF writes all messages to ``stderr``. ``stdout`` is reserved for piping -output files. ``stdin`` is reserved for piping input files. +OCRmyPDF writes all messages to ``stderr``. ``stdout`` is reserved for +piping output files. ``stdin`` is reserved for piping input files. -The return codes generated by the OCRmyPDF are considered part of the stable -user interface. They may be imported from ``ocrmypdf.exceptions``. +The return codes generated by the OCRmyPDF are considered part of the +stable user interface. They may be imported from +``ocrmypdf.exceptions``. .. list-table:: Return codes :widths: 5 35 60 @@ -218,22 +301,44 @@ user interface. They may be imported from ``ocrmypdf.exceptions``. Debugging the intermediate files --------------------------------- +================================ -OCRmyPDF normally saves its intermediate results to a temporary folder and deletes this folder when it exits, whether it succeeded or failed. +OCRmyPDF normally saves its intermediate results to a temporary folder +and deletes this folder when it exits, whether it succeeded or failed. -If the ``-k`` argument is issued on the command line, OCRmyPDF will keep the temporary folder and print the location, whether it succeeded or failed (provided the Python interpreter did not crash). An example message is: +If the ``-k`` argument is issued on the command line, OCRmyPDF will keep +the temporary folder and print the location, whether it succeeded or +failed (provided the Python interpreter did not crash). An example +message is: .. code-block:: none - Temporary working files saved at: - /tmp/com.github.ocrmypdf.u20wpz07 + Temporary working files retained at: + /tmp/ocrmypdf.io.u20wpz07 -The organization of this folder is an implementation detail and subject to change between releases. However the general organization is that working files on a per page basis have the page number as a prefix (starting with page 1), an infix indicates the processing stage, and a suffix indicates the file type. Some important files include: +The organization of this folder is an implementation detail and subject +to change between releases. However the general organization is that +working files on a per page basis have the page number as a prefix +(starting with page 1), an infix indicates the processing stage, and a +suffix indicates the file type. Some important files include: -* ``.page.png`` - what the input page looks like -* ``.image`` - the image we will show the user if we are in a mode that changes the final appearance; may be in one of several image formats -* ``.text.pdf`` - the OCR file; this will load as a blank page but should have visible text if checked with a tool like pdftotext or pdfminder.six -* ``.ocr.png`` - the file that is sent to Tesseract for OCR; depending on arguments this may differ from the presentation image -* ``layers.rendered.pdf`` - the composite PDF, before metadata repair and optimization -* ``images/*`` - images extracted during the optimization process; here the prefix indicates a PDF object ID not a page number +- ``_rasterize.png`` - what the input page looks like +- ``_ocr.png`` - the file that is sent to Tesseract for OCR; depending + on arguments this may differ from the presentation image +- ``_pp_deskew.png`` - the image, after deskewing +- ``_pp_clean.png`` - the image, after cleaning with unpaper +- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible + text embedded +- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page, + if the page is mixed format) +- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo + data structure +- ``graft_layers.pdf`` - the rendered PDF with OCR layers grafted on +- ``pdfa.pdf`` - ``graft_layers.pdf`` after conversion to PDF/A +- ``pdfa.ps`` - a PostScript file used by Ghostscript for PDF/A conversion +- ``optimize.pdf`` - the PDF generated before optimization +- ``optimize.out.pdf`` - the PDF generated by optimization +- ``origin`` - the input file +- ``origin.pdf`` - the input file or the input image converted to PDF +- ``images/*`` - images extracted during the optimization process; here + the prefix indicates a PDF object ID not a page number diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..3459e8b8 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,117 @@ +====================== +Using the OCRmyPDF API +====================== + +OCRmyPDF originated as a command line program and continues to have this +legacy, but parts of it can be imported and used in other Python +applications. + +Some applications may want to consider running ocrmypdf from a +subprocess call anyway, as this provides isolation of its activities. + +Example +======= + +OCRmyPDF one high-level function to run its main engine from an +application. The parameters are symmetric to the command line arguments +and largely have the same functions. + +.. code-block:: python + + import ocrmypdf + + if __name__ == '__main__': # To ensure correct behavior on Windows and macOS + ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True) + +With a few exceptions, all of the command line arguments are available +and may be passed as equivalent keywords. + +A few differences are that ``verbose`` and ``quiet`` are not available. +Instead, output should be managed by configuring logging. + +Parent process requirements +--------------------------- + +The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line +execution. To do this, it will: + +- create a monitoring thread +- create worker processes (on Linux, forking itself; on Windows and macOS, by + spawning) +- manage the signal flags of its worker processes +- execute other subprocesses (forking and executing other programs) + +The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently +privileged to perform these actions. + +There is no currently no option to manage how jobs are scheduled other +than the argument ``jobs=`` which will limit the number of worker +processes. + +Creating a child process to call ``ocrmypdf.ocr()`` is suggested. That +way your application will survive and remain interactive even if +OCRmyPDF fails for any reason. + +Programs that call ``ocrmypdf.ocr()`` should also install a SIGBUS signal +handler (except on Windows), to raise an exception if access to a memory +mapped file fails. OCRmyPDF may use memory mapping. + +``ocrmypdf.ocr()`` will take a threading lock to prevent multiple runs of itself +in the same Python interpreter process. This is not thread-safe, because of how +OCRmyPDF's plugins and Python's library import system work. If you need to parallelize +OCRmyPDF, use processes. + +.. warning:: + + On Windows and macOS, the script that calls ``ocrmypdf.ocr()`` must be + protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do + not take at least one of these steps, process semantics will prevent + OCRmyPDF from working correctly. + +Logging +------- + +OCRmyPDF will log under loggers named ``ocrmypdf``. In addition, it +imports ``pdfminer`` and ``PIL``, both of which post log messages under +those logging namespaces. + +You can configure the logging as desired for your application or call +:func:`ocrmypdf.configure_logging` to configure logging the same way +OCRmyPDF itself does. The command line parameters such as ``--quiet`` +and ``--verbose`` have no equivalents in the API; you must use the +provided configuration function or do configuration in a way that suits +your use case. + +Progress monitoring +------------------- + +OCRmyPDF uses the ``tqdm`` package to implement its progress bars. +:func:`ocrmypdf.configure_logging` will set up logging output to +``sys.stderr`` in a way that is compatible with the display of the +progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable +the progress bar. + +Exceptions +---------- + +OCRmyPDF may throw standard Python exceptions, ``ocrmypdf.exceptions.*`` +exceptions, some exceptions related to multiprocessing, and +``KeyboardInterrupt``. The parent process should provide an exception +handler. OCRmyPDF will clean up its temporary files and worker processes +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. + +Reference +--------- + +.. autofunction:: ocrmypdf.ocr + +.. autoclass:: ocrmypdf.Verbosity + :members: + :undoc-members: + +.. autofunction:: ocrmypdf.configure_logging diff --git a/docs/apiref.rst b/docs/apiref.rst new file mode 100644 index 00000000..5a3bd488 --- /dev/null +++ b/docs/apiref.rst @@ -0,0 +1,55 @@ +============= +API Reference +============= + +This page summarizes the rest of the public API. Generally speaking this +should mainly of interest to plugin developers. + +ocrmypdf +======== + +.. autoclass:: ocrmypdf.PageContext + :members: + +.. autoclass:: ocrmypdf.PdfContext + :members: + +ocrmypdf.exceptions +=================== + +.. automodule:: ocrmypdf.exceptions + :members: + :undoc-members: + +ocrmypdf.helpers +================ + +.. automodule:: ocrmypdf.helpers + :members: + :noindex: deprecated + + .. autodecorator:: deprecated + +ocrmypdf.hocrtransform +====================== + +.. automodule:: ocrmypdf.hocrtransform + :members: + +ocrmypdf.pdfa +============= + +.. automodule:: ocrmypdf.pdfa + :members: + +ocrmypdf.quality +================ + +.. automodule:: ocrmypdf.quality + :members: + +ocrmypdf.subprocess +=================== + +.. automodule:: ocrmypdf.subprocess + :members: diff --git a/docs/batch.rst b/docs/batch.rst index 6883e992..e8e97374 100644 --- a/docs/batch.rst +++ b/docs/batch.rst @@ -1,225 +1,217 @@ +================ Batch processing ================ -This article provides information about running OCRmyPDF on multiple files or configuring it as a service triggered by file system events. +This article provides information about running OCRmyPDF on multiple +files or configuring it as a service triggered by file system events. Batch jobs ----------- +========== -Consider using the excellent `GNU Parallel `_ to apply OCRmyPDF to multiple files at once. +Consider using the excellent `GNU +Parallel `__ to apply OCRmyPDF +to multiple files at once. -Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once. +Both ``parallel`` and ``ocrmypdf`` will try to use all available +processors. To maximize parallelism without overloading your system with +processes, consider using ``parallel -j 2`` to limit parallel to running +two jobs at once. -This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder. It will not search subdirectories. +This command will run all ocrmypdf all files named ``*.pdf`` in the +current directory and write them to the previous created ``output/`` +folder. It will not search subdirectories. -The ``--tag`` argument tells parallel to print the filename as a prefix whenever a message is printed, so that one can trace any errors to the file that produced them. +The ``--tag`` argument tells parallel to print the filename as a prefix +whenever a message is printed, so that one can trace any errors to the +file that produced them. .. code-block:: bash - parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf + parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf -OCRmyPDF automatically repairs PDFs before parsing and gathering information from them. +OCRmyPDF automatically repairs PDFs before parsing and gathering +information from them. Directory trees ---------------- +=============== -This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes +This will walk through a directory tree and run OCR on all files in +place, printing the output in a way that makes .. code-block:: bash - find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \; - -Alternatively, with a docker container (mounts a volume to the container where the PDFs are stored): + find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \; + +Alternatively, with a docker container (mounts a volume to the container +where the PDFs are stored): .. code-block:: bash - find . -printf '%p' -name '*.pdf' -exec docker run --rm -v : jbarlow83/ocrmypdf-alpine '/{}' '/{}' \; + find . -printf '%p' -name '*.pdf' -exec docker run --rm -v : jbarlow83/ocrmypdf '/{}' '/{}' \; -This only runs one ``ocrmypdf`` process at a time. This variation uses ``find`` to create a directory list and ``parallel`` to parallelize runs of ``ocrmypdf``, again updating files in place. +This only runs one ``ocrmypdf`` process at a time. This variation uses +``find`` to create a directory list and ``parallel`` to parallelize runs +of ``ocrmypdf``, again updating files in place. .. code-block:: bash - find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}' + find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}' +In a Windows batch file, use + +.. code-block:: bat + + for /r %%f in (*.pdf) do ocrmypdf %%f %%f Sample script -""""""""""""" +------------- -This user contributed script also provides an example of batch processing. - -.. code-block:: python - - #!/usr/bin/env python3 - # Walk through directory tree, replacing all files with OCR'd version - # Contributed by DeliciousPickle@github - - import logging - import os - import subprocess - import sys - - script_dir = os.path.dirname(os.path.realpath(__file__)) - print(script_dir + '/ocr-tree.py: Start') - - if len(sys.argv) > 1: - start_dir = sys.argv[1] - else: - start_dir = '.' - - if len(sys.argv) > 2: - log_file = sys.argv[2] - else: - log_file = script_dir + '/ocr-tree.log' - - logging.basicConfig( - level=logging.INFO, format='%(asctime)s %(message)s', - filename=log_file, filemode='w') - - for dir_name, subdirs, file_list in os.walk(start_dir): - logging.info('\n') - logging.info(dir_name + '\n') - os.chdir(dir_name) - for filename in file_list: - file_ext = os.path.splitext(filename)[1] - if file_ext == '.pdf': - full_path = dir_name + '/' + filename - print(full_path) - cmd = ["ocrmypdf", "--deskew", filename, filename] - logging.info(cmd) - proc = subprocess.run( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - result = proc.stdout - if proc.returncode == 6: - print("Skipped document because it already contained text") - elif proc.returncode == 0: - print("OCR complete") - logging.info(result) - -API -""" - -OCRmyPDF is currently supported as a command line interface. This means that even if you are using OCRmyPDF in a Python script, you should run it in a subprocess rather importing the ocrmypdf package. - -The reason for this limitation is that the `ruffus `_ library that OCRmyPDF depends on is unfortunately not reentrant. OCRmyPDF works by defining each operation it does as a ruffus task that takes one or more files as input and generates one or more files as output. As such ruffus is fairly fundamental. - -(If you find individual functions implemented in OCRmyPDF useful (such as ``ocrmypdf.pdfinfo``), you can use these if you wish to.) +This user contributed script also provides an example of batch +processing. +.. literalinclude:: ../misc/batch.py + :caption: misc/batch.py Synology DiskStations -""""""""""""""""""""" - -Synology DiskStations (Network Attached Storage devices) can run the Docker image of OCRmyPDF if the Synology `Docker package `_ is installed. Attached is a script to address particular quirks of using OCRmyPDF on one of these devices. - -This is only possible for x86-based Synology products. Some Synology products use ARM or Power processors and do not support Docker. Further adjustments might be needed to deal with the Synology's relatively limited CPU and RAM. - -.. code-block:: python - - #!/bin/env python3 - # Contributed by github.com/Enantiomerie - - # script needs 2 arguments - # 1. source dir with *.pdf - default is location of script - # 2. move dir where *.pdf and *_OCR.pdf are moved to - - import logging - import os - import subprocess - import sys - import time - import shutil - - script_dir = os.path.dirname(os.path.realpath(__file__)) - timestamp = time.strftime("%Y-%m-%d-%H%M_") - log_file = script_dir + '/' + timestamp + 'ocrmypdf.log' - logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', filename=log_file, filemode='w') - - if len(sys.argv) > 1: - start_dir = sys.argv[1] - else: - start_dir = '.' - - for dir_name, subdirs, file_list in os.walk(start_dir): - logging.info('\n') - logging.info(dir_name + '\n') - os.chdir(dir_name) - for filename in file_list: - file_ext = os.path.splitext(filename)[1] - if file_ext == '.pdf': - full_path = dir_name + '/' + filename - file_noext = os.path.splitext(filename)[0] - timestamp_OCR = time.strftime("%Y-%m-%d-%H%M_OCR_") - filename_OCR = timestamp_OCR + file_noext + '.pdf' - docker_mount = dir_name + ':/home/docker' - # create string for pdf processing - # diskstation needs a user:group docker:docker. find uid:gid of your diskstation docker:docker with id docker. - # use this uid:gid in -u flag - # rw rights for docker:docker at source dir are also necessary - # the script is processed as root user via chron - cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR] - logging.info(cmd) - proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - result = proc.stdout.read() - logging.info(result) - full_path_OCR = dir_name + '/' + filename_OCR - os.chmod(full_path_OCR, 0o666) - os.chmod(full_path, 0o666) - full_path_OCR_archive = sys.argv[2] - full_path_archive = sys.argv[2] + '/no_ocr' - shutil.move(full_path_OCR,full_path_OCR_archive) - shutil.move(full_path, full_path_archive) - logging.info('Finished.\n') - -Huge batch jobs -""""""""""""""" - -If you have thousands of files to work with, contact the author. Consulting work related to OCRmyPDF helps fund this open source project and all inquiries are appreciated. - -Hot (watched) folders --------------------- -To set up a "hot folder" that will trigger OCR for every file inserted, use a program like Python `watchdog `_ (supports all major OS). +Synology DiskStations (Network Attached Storage devices) can run the +Docker image of OCRmyPDF if the Synology `Docker +package `__ is +installed. Attached is a script to address particular quirks of using +OCRmyPDF on one of these devices. -One could then configure a scanner to automatically place scanned files in a hot folder, so that they will be queued for OCR and copied to the destination. +This is only possible for x86-based Synology products. Some Synology +products use ARM or Power processors and do not support Docker. Further +adjustments might be needed to deal with the Synology's relatively +limited CPU and RAM. -.. code-block:: bash +.. literalinclude:: ../misc/synology.py + :caption: misc/synology.py - Sample script for Synology DiskStations - pip install watchdog - -watchdog installs the command line program ``watchmedo``, which can be told to run ``ocrmypdf`` on any .pdf added to the current directory (``.``) and place the result in the previously created ``out/`` folder. - -.. code-block:: bash - - cd hot-folder - mkdir out - watchmedo shell-command \ - --patterns="*.pdf" \ - --ignore-directories \ - --command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \ - . # don't forget the final dot - -For more complex behavior you can write a Python script around to use the watchdog API. - -On file servers, you could configure watchmedo as a system service so it will run all the time. - -Caveats -""""""" - -* ``watchmedo`` may not work properly on a networked file system, depending on the capabilities of the file system client and server. -* This simple recipe does not filter for the type of file system event, so file copies, deletes and moves, and directory operations, will all be sent to ocrmypdf, producing errors in several cases. Disable your watched folder if you are doing anything other than copying files to it. -* If the source and destination directory are the same, watchmedo may create an infinite loop. -* On BSD, FreeBSD and older versions of macOS, you may need to increase the number of file descriptors to monitor more files, using ``ulimit -n 1024`` to watch a folder of up to 1024 files. - -Alternatives -"""""""""""" - -* `Watchman `_ is a more powerful alternative to ``watchmedo``. - -macOS Automator +Huge batch jobs --------------- -You can use the Automator app with macOS, to create a Workflow or Quick Action. Use a *Run Shell Script* action in your workflow. In the context of Automator, the ``PATH`` may be set differently your Terminal's ``PATH``; you may need to explicitly set the PATH to include ``ocrmypdf``. The following example may serve as a starting point: +If you have thousands of files to work with, contact the author. +Consulting work related to OCRmyPDF helps fund this open source project +and all inquiries are appreciated. -.. image:: images/macos-workflow.png - :alt: Example macOS Automator script +Hot (watched) folders +===================== + +Watched folders with watcher.py +------------------------------- + +OCRmyPDF has a folder watcher called watcher.py, which is currently included in source +distributions but not part of the main program. It may be used natively or may run +in a Docker container. Native instances tend to give better performance. watcher.py +works on all platforms. + +Users may need to customize the script to meet their requirements. + +.. code-block:: bash + + pip3 install -r requirements/watcher.txt + + env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \ + OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \ + OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \ + python3 watcher.py + +.. csv-table:: watcher.py environment variables + :header: "Environment variable", "Description" + :widths: 50, 50 + + "OCR_INPUT_DIRECTORY", "Set input directory to monitor (recursive)" + "OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)" + "OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)" + "OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``" + "OCR_DESKEW", "Apply deskew to crooked input PDFs" + "OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``." + "OCR_POLL_NEW_FILE_SECONDS", "Polling interval" + "OCR_LOGLEVEL", "Level of log messages to report" + +One could configure a networked scanner or scanning computer to drop files in the +watched folder. + +Watched folders with Docker +--------------------------- + +The watcher service is included in the OCRmyPDF Docker image. To run it: + +.. code-block:: bash + + docker run \ + -v :/input \ + -v :/output \ + -e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \ + -e OCR_ON_SUCCESS_DELETE=1 \ + -e OCR_DESKEW=1 \ + -e PYTHONUNBUFFERED=1 \ + -it --entrypoint python3 \ + jbarlow83/ocrmypdf \ + watcher.py + +This service will watch for a file that matches ``/input/\*.pdf`` and will +convert it to a OCRed PDF in ``/output/``. The parameters to this image are: + +.. csv-table:: watcher.py parameters for Docker + :header: "Parameter", "Description" + :widths: 50, 50 + + "``-v :/input``", "Files placed in this location will be OCRed" + "``-v :/output``", "This is where OCRed files will be stored" + "``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1" + "``-e OCR_ON_SUCCESS_DELETE=1``", "Define environment variable" + "``-e OCR_DESKEW=1``", "Define environment variable" + "``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs" + +This service relies on polling to check for changes to the filesystem. It +may not be suitable for some environments, such as filesystems shared on a +slow network. + +A configuration manager such as Docker Compose could be used to ensure that the +service is always available. + +.. literalinclude:: ../misc/docker-compose.example.yml + :language: yaml + :caption: misc/docker-compose.example.yml + +Caveats +------- + +- ``watchmedo`` may not work properly on a networked file system, + depending on the capabilities of the file system client and server. +- This simple recipe does not filter for the type of file system event, + so file copies, deletes and moves, and directory operations, will all + be sent to ocrmypdf, producing errors in several cases. Disable your + watched folder if you are doing anything other than copying files to + it. +- If the source and destination directory are the same, watchmedo may + create an infinite loop. +- On BSD, FreeBSD and older versions of macOS, you may need to increase + the number of file descriptors to monitor more files, using + ``ulimit -n 1024`` to watch a folder of up to 1024 files. + +Alternatives +------------ + +- On Linux, `systemd user services `__ + can be configured to automatically perform OCR on a collection of files. + +- `Watchman `__ is a more + powerful alternative to ``watchmedo``. + +macOS Automator +=============== + +You can use the Automator app with macOS, to create a Workflow or Quick +Action. Use a *Run Shell Script* action in your workflow. In the context +of Automator, the ``PATH`` may be set differently your Terminal's +``PATH``; you may need to explicitly set the PATH to include +``ocrmypdf``. The following example may serve as a starting point: + +.. figure:: images/macos-workflow.png + :alt: Example macOS Automator workflow You may customize the command sent to ocrmypdf. diff --git a/docs/conf.py b/docs/conf.py index f1f32a01..4a6dc0ac 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # ocrmypdf documentation build configuration file, created by # sphinx-quickstart on Sun Sep 4 14:29:43 2016. @@ -21,6 +20,8 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +"""isort:skip_file""" + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -30,9 +31,9 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - # 'sphinx.ext.mathjax', -] +extensions = ['sphinx.ext.napoleon'] + +napoleon_use_rtype = False # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -53,7 +54,7 @@ master_doc = 'index' # General information about the project. project = 'ocrmypdf' copyright = ( - '2019, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.' + '2020, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.' ) author = 'James R. Barlow' @@ -92,6 +93,7 @@ from pkg_resources import get_distribution, DistributionNotFound release = get_distribution('ocrmypdf').version version = '.'.join(release.split('.')[:2]) + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # @@ -176,7 +178,7 @@ html_theme_options = {'display_version': False} # The name of an image file (relative to this directory) to place at the top # of the sidebar. # -# html_logo = None +# html_logo = "images/logo.svg" # looks bad # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 00000000..6c928933 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,67 @@ +======================= +Contributing guidelines +======================= + +Contributions are welcome! + +Big changes +=========== + +Please open a new issue to discuss or propose a major change. Not only is it fun +to discuss big ideas, but we might save each other's time too. Perhaps some of the +work you're contemplating is already half-done in a development branch. + +Code style +========== + +We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The +settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull +requests should follow the style guide. One difference we use from "black" style +is that strings shown to the user are always in double quotes (``"``) and strings +for internal uses are in single quotes (``'``). + +Tests +===== + +New features should come with tests that confirm their correctness. + +New Python dependencies +======================= + +If you are proposing a change that will require a new Python dependency, we +prefer dependencies that are already packaged by Debian or Red Hat. This makes +life much easier for our downstream package maintainers. + +Python dependencies must also be license-compatible. GPLv3 or AGPLv3 are likely +incompatible with the project's license, but LGPLv3 is compatible. + +New non-Python dependencies +=========================== + +OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for +its functionality. In general we prefer to avoid adding new external programs. + +Style guide: Is it OCRmyPDF or ocrmypdf? +======================================== + +The program/project is OCRmyPDF and the name of the executable or library is ocrmypdf. + +Known ports/packagers +===================== + +OCRmyPDF has been ported to many platforms already. If you are interesting in +porting to a new platform, check with +`Repology `__ to see the status +of that platform. + +Packager maintainers, please ensure that the command line completion scripts in +``misc/`` are installed. + +Copyright and license +===================== + +For contributions over 10 lines of code, please include your name to list of +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 +MIT. Please contribute code only that you wrote and you have the permission to +contribute or license to us. diff --git a/docs/cookbook.rst b/docs/cookbook.rst index 3ea153aa..8ee4d8fd 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -1,11 +1,12 @@ +======== Cookbook ======== Basic examples --------------- +============== Help! -^^^^^ +----- ocrmypdf has built-in help. @@ -13,30 +14,29 @@ ocrmypdf has built-in help. ocrmypdf --help - Add an OCR layer and convert to PDF/A -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------------- .. code-block:: bash ocrmypdf input.pdf output.pdf Add an OCR layer and output a standard PDF -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------------------ .. code-block:: bash ocrmypdf --output-type pdf input.pdf output.pdf Create a PDF/A with all color and grayscale images converted to JPEG -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------------------------------------------- .. code-block:: bash ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf Modify a file in place -^^^^^^^^^^^^^^^^^^^^^^ +---------------------- The file will only be overwritten if OCRmyPDF is successful. @@ -45,48 +45,76 @@ The file will only be overwritten if OCRmyPDF is successful. ocrmypdf myfile.pdf myfile.pdf Correct page rotation -^^^^^^^^^^^^^^^^^^^^^ +--------------------- -OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages. +OCR will attempt to automatic correct the rotation of each page. This +can help fix a scanning job that contains a mix of landscape and +portrait pages. .. code-block:: bash ocrmypdf --rotate-pages myfile.pdf myfile.pdf -You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive. +You can increase (decrease) the parameter ``--rotate-pages-threshold`` +to make page rotation more (less) aggressive. The threshold number is the ratio +of how confidence the OCR engine is that the document image should be changed, +compared to kept the same. The default value is quite conservative; on some files +it may not attempt rotations at all unless it is very confident that the current +rotation is wrong. A lower value of ``2.0`` will produce more rotations, and +more false positives. Run with ``-v1`` to see the confidence level for each +page to see if there may be a better value for your files. -If the page is "just a little off horizontal", like a crooked picture, then you want ``--deskew``. ``--rotate-pages`` is for when the cardinal angle is wrong. +If the page is "just a little off horizontal", like a crooked picture, +then you want ``--deskew``. ``--rotate-pages`` is for when the cardinal +angle is wrong. OCR languages other than English -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------- -OCRmyPDF assumes the document is in English unless told otherwise. OCR quality may be poor if the wrong language is used. +OCRmyPDF assumes the document is in English unless told otherwise. OCR +quality may be poor if the wrong language is used. .. code-block:: bash ocrmypdf -l fra LeParisien.pdf LeParisien.pdf ocrmypdf -l eng+fra Bilingual-English-French.pdf Bilingual-English-French.pdf -Language packs must be installed for all languages specified. See :ref:`Installing additional language packs `. +Language packs must be installed for all languages specified. See +:ref:`Installing additional language packs `. -Unfortunately, the Tesseract OCR engine has no ability to detect the language when it is unknown. +Unfortunately, the Tesseract OCR engine has no ability to detect the +language when it is unknown. Produce PDF and text file containing OCR text -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +--------------------------------------------- -This produces a file named "output.pdf" and a companion text file named "output.txt". +This produces a file named "output.pdf" and a companion text file named +"output.txt". .. code-block:: bash ocrmypdf --sidecar output.txt input.pdf output.pdf +.. note:: + + The sidecar file contains the **OCR text** found by OCRmyPDF. If the document + contains pages that already have text, that text will not appear in the + sidecar. If the option ``--pages`` is used, only those pages on which OCR + was performed will be included in the sidecar. If certain pages were skipped + because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages + will not be in the sidecar. + + To extract all text from a PDF, whether generated from OCR or otherwise, + use a program like Poppler's ``pdftotext`` or ``pdfgrep``. + OCR images, not PDFs -^^^^^^^^^^^^^^^^^^^^ +-------------------- Option: use Tesseract -""""""""""""""""""""" +~~~~~~~~~~~~~~~~~~~~~ -If you are starting with images, you can just use Tesseract directly to convert images to PDFs: +If you are starting with images, you can just use Tesseract directly to +convert images to PDFs: .. code-block:: bash @@ -97,62 +125,88 @@ If you are starting with images, you can just use Tesseract directly to convert # When there are multiple images tesseract text-file-containing-list-of-image-filenames.txt output-prefix pdf -Tesseract's PDF output is quite good – OCRmyPDF uses it internally, in some cases. However, OCRmyPDF has many features not available in Tesseract like image processing, metadata control, and PDF/A generation. +Tesseract's PDF output is quite good – OCRmyPDF uses it internally, in +some cases. However, OCRmyPDF has many features not available in +Tesseract like image processing, metadata control, and PDF/A generation. Option: use img2pdf -""""""""""""""""""" +~~~~~~~~~~~~~~~~~~~ -You can also use a program like `img2pdf `_ to convert your images to PDFs, and then pipe the results to run ocrmypdf. The ``-`` tells ocrmypdf to read standard input. +You can also use a program like +`img2pdf `__ to convert +your images to PDFs, and then pipe the results to run ocrmypdf. The +``-`` tells ocrmypdf to read standard input. .. code-block:: bash img2pdf my-images*.jpg | ocrmypdf - myfile.pdf -``img2pdf`` is recommended because it does an excellent job at generating PDFs without transcoding images. +``img2pdf`` is recommended because it does an excellent job at +generating PDFs without transcoding images. Option: use OCRmyPDF (single images only) -""""""""""""""""""""""""""""""""""""""""" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For convenience, OCRmyPDF can also convert single images to PDFs on its own. If the resolution (dots per inch, DPI) of an image is not set or is incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 cm, 1 dpi = 0.39 dpcm). +For convenience, OCRmyPDF can also convert single images to PDFs on its +own. If the resolution (dots per inch, DPI) of an image is not set or is +incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 +cm, 1 dpi = 0.39 dpcm). .. code-block:: bash ocrmypdf --image-dpi 300 image.png myfile.pdf -If you have multiple images, you must use ``img2pdf`` to convert the images to PDF. +If you have multiple images, you must use ``img2pdf`` to convert the +images to PDF. Not recommended -""""""""""""""" +~~~~~~~~~~~~~~~ -We caution against using ImageMagick or Ghostscript to convert images to PDF, since they may transcode images or produce downsampled images, sometimes without warning. +We caution against using ImageMagick or Ghostscript to convert images to +PDF, since they may transcode images or produce downsampled images, +sometimes without warning. Image processing ----------------- +================ -OCRmyPDF perform some image processing on each page of a PDF, if desired. The same processing is applied to each page. It is suggested that the user review files after image processing as these commands might remove desirable content, especially from poor quality scans. +OCRmyPDF perform some image processing on each page of a PDF, if +desired. The same processing is applied to each page. It is suggested +that the user review files after image processing as these commands +might remove desirable content, especially from poor quality scans. -* ``--rotate-pages`` attempts to determine the correct orientation for each page and rotates the page if necessary. - -* ``--remove-background`` attempts to detect and remove a noisy background from grayscale or color images. Monochrome images are ignored. This should not be used on documents that contain color photos as it may remove them. - -* ``--deskew`` will correct pages were scanned at a skewed angle by rotating them back into place. Skew determination and correction is performed using `Postl's variance of line sums `_ algorithm as implemented in `Leptonica `_. - -* ``--clean`` uses `unpaper `_ to clean up pages before OCR, but does not alter the final output. This makes it less likely that OCR will try to find text in background noise. - -* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important. - -* ``--mask-barcodes`` will suppress any barcodes detected in a page image. Barcodes are known to confuse Tesseract OCR and interfere with the recognition of text on the same baseline as a barcode. The output file will contain the unaltered image of the barcode. +- ``--rotate-pages`` attempts to determine the correct orientation for + each page and rotates the page if necessary. +- ``--remove-background`` attempts to detect and remove a noisy + background from grayscale or color images. Monochrome images are + ignored. This should not be used on documents that contain color + photos as it may remove them. +- ``--deskew`` will correct pages were scanned at a skewed angle by + rotating them back into place. Skew determination and correction is + performed using `Postl's variance of line + sums `__ algorithm as + implemented in `Leptonica `__. +- ``--clean`` uses + `unpaper `__ to clean up + pages before OCR, but does not alter the final output. This makes it + less likely that OCR will try to find text in background noise. +- ``--clean-final`` uses unpaper to clean up pages before OCR and + inserts the page into the final output. You will want to review each + page to ensure that unpaper did not remove something important. .. note:: - In many cases image processing will rasterize PDF pages as images, potentially losing quality. + In many cases image processing will rasterize PDF pages as images, + potentially losing quality. .. warning:: - ``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options. + ``--clean-final`` and ``-remove-background`` may leave undesirable + visual artifacts in some images where their algorithms have + shortcomings. Files should be visually reviewed after using these + options. Example: OCR and correct document skew (crooked scan) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +----------------------------------------------------- Deskew: @@ -160,55 +214,118 @@ Deskew: ocrmypdf --deskew input.pdf output.pdf -Image processing commands can be combined. The order in which options are given does not matter. OCRmyPDF always applies the steps of the image processing pipeline in the same order (rotate, remove background, deskew, clean). +Image processing commands can be combined. The order in which options +are given does not matter. OCRmyPDF always applies the steps of the +image processing pipeline in the same order (rotate, remove background, +deskew, clean). .. code-block:: bash ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf - Don't actually OCR my PDF -------------------------- +========================= -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 or PDF/A conversion. +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 or PDF/A conversion. .. code-block:: bash ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf +Optimize images without performing OCR +-------------------------------------- + +You can also optimize all images without performing any OCR: + +.. code-block:: bash + + ocrmypdf --tesseract-timeout=0 --optimize 3 --skip-text input.pdf output.pdf + +Perform OCR only certain pages +------------------------------ + +You can ask OCRmyPDF to only apply OCR to certain pages. + +.. code-block:: bash + + ocrmypdf --pages 2,3,13-17 input.pdf output.pdf + +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'``. + +OCRmyPDF will warn if your list of page numbers contains duplicates or +overlap pages. OCRmyPDF does not currently account for document page numbers, +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. + +Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages in +the file and convert it to PDF/A, unless you disable those options. In this +example, we want to OCR only the title and otherwise change the PDF as little +as possible: + +.. code-block:: bash + + ocrmypdf --pages 1 --output-type pdf --optimize 0 input.pdf output.pdf Redo existing OCR ------------------ +================= -To redo OCR on a file OCRed with other OCR software or a previous version of OCRmyPDF and/or Tesseract, you may use the ``--redo-ocr`` argument. (Normally, OCRmyPDF will exit with an error if asked to modify a file with OCR.) +To redo OCR on a file OCRed with other OCR software or a previous +version of OCRmyPDF and/or Tesseract, you may use the ``--redo-ocr`` +argument. (Normally, OCRmyPDF will exit with an error if asked to modify +a file with OCR.) -This may be helpful for users who want to take advantage of accuracy improvements in Tesseract 4.0 for files they previously OCRed with an earlier version of Tesseract and OCRmyPDF. +This may be helpful for users who want to take advantage of accuracy +improvements in Tesseract 4.0 for files they previously OCRed with an +earlier version of Tesseract and OCRmyPDF. .. code-block:: bash ocrmypdf --redo-ocr input.pdf output.pdf -This method will replace OCR without rasterizing, reducing quality or removing vector content. If a file contains a mix of pure digital text and OCR, digital text will be ignored and OCR will be replaced. As such this mode is incompatible with image processing options, since they alter the appearance of the file. +This method will replace OCR without rasterizing, reducing quality or +removing vector content. If a file contains a mix of pure digital text +and OCR, digital text will be ignored and OCR will be replaced. As such +this mode is incompatible with image processing options, since they +alter the appearance of the file. -In some cases, existing OCR cannot be detected or replaced. Files produced by OCRmyPDF v2.2 or earlier, for example, are internally represented as having visible text with an opaque image drawn on top. This situation cannot be detected. +In some cases, existing OCR cannot be detected or replaced. Files +produced by OCRmyPDF v2.2 or earlier, for example, are internally +represented as having visible text with an opaque image drawn on top. +This situation cannot be detected. -If ``--redo-ocr`` does not work, you can use ``--force-ocr``, which will force rasterization of all pages, potentially reducing quality or losing vector content. +If ``--redo-ocr`` does not work, you can use ``--force-ocr``, which will +force rasterization of all pages, potentially reducing quality or losing +vector content. Improving OCR quality ---------------------- +===================== -The `Image processing`_ features can improve OCR quality. +The `Image processing <#image-processing>`__ features can improve OCR +quality. -Rotating pages and deskewing helps to ensure that the page orientation is correct before OCR begins. Removing the background and/or cleaning the page can also improve results. The ``--oversample DPI`` argument can be specified to resample images to higher resolution before attempting OCR; this can improve results as well. +Rotating pages and deskewing helps to ensure that the page orientation +is correct before OCR begins. Removing the background and/or cleaning +the page can also improve results. The ``--oversample DPI`` argument can +be specified to resample images to higher resolution before attempting +OCR; this can improve results as well. -OCR quality will suffer if the resolution of input images is not correct (since the range of pixel sizes that will be checked for possible fonts will also be incorrect). +OCR quality will suffer if the resolution of input images is not correct +(since the range of pixel sizes that will be checked for possible fonts +will also be incorrect). PDF optimization ----------------- +================ -By default OCRmyPDF will attempt to perform lossless optimizations on the images inside PDFs after OCR is complete. Optimization is performed even if no OCR text is found. +By default OCRmyPDF will attempt to perform lossless optimizations on +the images inside PDFs after OCR is complete. Optimization is performed +even if no OCR text is found. -The ``--optimize N`` (short form ``-O``) argument controls optimization, where ``N`` ranges from 0 to 3 inclusive, analogous to the optimization levels in the GCC compiler. +The ``--optimize N`` (short form ``-O``) argument controls optimization, +where ``N`` ranges from 0 to 3 inclusive, analogous to the optimization +levels in the GCC compiler. .. list-table:: :widths: auto @@ -227,9 +344,15 @@ The ``--optimize N`` (short form ``-O``) argument controls optimization, where ` * - ``--optimize 3`` - All of the above, and enables more aggressive optimizations and targets lower image quality. -Optimization is improved when a JBIG2 encoder is available and when ``pngquant`` is installed. If either of these components are missing, then some types of images cannot be optimized. +Optimization is improved when a JBIG2 encoder is available and when +``pngquant`` is installed. If either of these components are missing, +then some types of images cannot be optimized. -The types of optimization available may expand over time. By default, OCRmyPDF compresses data streams inside PDFs, and will change inefficient compression modes to more modern versions. A program like ``qpdf`` can be used to change encodings, e.g. to inspect the internals fo a PDF. +The types of optimization available may expand over time. By default, +OCRmyPDF compresses data streams inside PDFs, and will change +inefficient compression modes to more modern versions. A program like +``qpdf`` can be used to change encodings, e.g. to inspect the internals +fo a PDF. .. code-block:: bash diff --git a/docs/docker.rst b/docs/docker.rst index 73f0a3e8..16eccb7c 100644 --- a/docs/docker.rst +++ b/docs/docker.rst @@ -1,155 +1,196 @@ +.. _docker: + +===================== OCRmyPDF Docker image ===================== -OCRmyPDF is also available in a Docker image that packages recent versions of all dependencies. +OCRmyPDF is also available in a Docker image that packages recent +versions of all dependencies. -For users who already have Docker installed this may be an easy and convenient option. However, it is less performant than a system installation and may require Docker engine configuration. +For users who already have Docker installed this may be an easy and +convenient option. However, it is less performant than a system +installation and may require Docker engine configuration. -OCRmyPDF needs a generous amount of RAM, CPU cores, and temporary storage space. +OCRmyPDF needs a generous amount of RAM, CPU cores, temporary storage +space, whether running in a Docker container or on its own. It may be +necessary to ensure the container is provisioned with additional +resources. .. _docker-install: Installing the Docker image ---------------------------- +=========================== -If you have `Docker `_ installed on your system, you can install a Docker image of the latest release. +If you have `Docker `__ installed on your +system, you can install a Docker image of the latest release. -The recommended OCRmyPDF Docker image is currently named ``ocrmypdf-alpine``: +If you can run this command successfully, your system is ready to download and +execute the image: .. code-block:: bash - docker pull jbarlow83/ocrmypdf-alpine + docker run hello-world -Follow the Docker installation instructions for your platform. If you can run this command successfully, your system is ready to download and execute the image: +The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``: .. code-block:: bash - docker run hello-world + docker pull jbarlow83/ocrmypdf -OCRmyPDF will use all available CPU cores. By default, the VirtualBox 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: + +OCRmyPDF will use all available CPU cores. By default, the VirtualBox +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") + # 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 `__. Using the Docker image on the command line ------------------------------------------- +========================================== -**Unlike typical Docker containers**, in this mode we are using the OCRmyPDF Docker container is intended to be emphemeral – it runs for one OCR job and then terminates, just like a command line program. We are using Docker as a way of delivering an application, not a server. +**Unlike typical Docker containers**, in this section the OCRmyPDF Docker +container is emphemeral – it runs for one OCR job and terminates, just like a +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 start a Docker container (instance of the image): .. code-block:: bash - docker tag jbarlow83/ocrmypdf-alpine ocrmypdf - docker run --rm ocrmypdf (... all other arguments here...) + docker tag jbarlow83/ocrmypdf ocrmypdf + docker run --rm -i ocrmypdf (... all other arguments here...) - - -For convenience, create a shell alias to hide the Docker command: +For convenience, create a shell alias to hide the Docker command. It is +easier to send the input file as stdin and read the output from +stdout – **this avoids the messy permission issues with Docker entirely**. .. code-block:: bash - alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf' - ocrmypdf --version # runs docker version + alias docker_ocrmypdf='docker run --rm -i ocrmypdf' + docker_ocrmypdf --version # runs docker version + docker_ocrmypdf - - output.pdf -Or in the wonderful `fish shell `_: +Or in the wonderful `fish shell `__: .. code-block:: fish - alias ocrmypdf 'docker run --rm ocrmypdf' - funcsave ocrmypdf + alias docker_ocrmypdf 'docker run --rm ocrmypdf' + funcsave docker_ocrmypdf + +Alternately, you could mount the local current working directory as a +Docker volume: + +.. code-block:: bash + + alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" ocrmypdf' + docker_ocrmypdf /data/input.pdf /data/output.pdf .. _docker-lang-packs: Adding languages to the Docker image ------------------------------------- +==================================== -By default the Docker image includes English, German and Simplified Chinese, the most popular languages for OCRmyPDF users based on feedback. You may add other languages by creating a new Dockerfile based on the public one: +By default the Docker image includes English, German, Simplified Chinese, +French, Portuguese and Spanish, the most popular languages for OCRmyPDF +users based on feedback. You may add other languages by creating a new +Dockerfile based on the public one. .. code-block:: dockerfile - FROM jbarlow83/ocrmypdf-alpine + FROM jbarlow83/ocrmypdf - # Add French - RUN apk add tesseract-ocr-data-fra + # Example: add Italian + RUN apt install tesseract-ocr-ita + +To install language packs (training data) such as the +`tessdata_best `_ suite or +custom data, you first need to determine the version of Tesseract data files, which +may differ from the Tesseract program version. Use this command to determine the data +file version: + +.. code-block:: bash + + docker run -i --rm --entrypoint /bin/ls jbarlow83/ocrmypdf /usr/share/tesseract-ocr + +As of 2021, the data file version is probably ``4.00``. + +You can then add new data with either a Dockerfile: + +.. code-block:: dockerfile + + FROM jbarlow83/ocrmypdf + + # Example: add a tessdata_best file + COPY chi_tra_vert.traineddata /usr/share/tesseract-ocr//tessdata/ + +Alternately, you can copy training data into a Docker container as follows: + +.. code-block:: bash + + docker cp mycustomtraining.traineddata name_of_container:/usr/share/tesseract-ocr//tessdata/ Executing the test suite ------------------------- +======================== -The OCRmyPDF test suite is installed with image. To run it: +The OCRmyPDF test suite is installed with image. To run it: .. code-block:: bash - docker run --entrypoint python3 jbarlow83/ocrmypdf-alpine setup.py test + docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest + +Accessing the shell +=================== + +To use the bash shell in the Docker image: + +.. code-block:: bash + + docker run -it --entrypoint bash jbarlow83/ocrmypdf Using the OCRmyPDF web service wrapper --------------------------------------- +====================================== -The OCRmyPDF Docker image includes an example, barebones HTTP web service. The webservice may be launched as follows: +The OCRmyPDF Docker image includes an example, barebones HTTP web +service. The webservice may be launched as follows: .. code-block:: bash - docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf-alpine webservice.py + docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py -Unlike command line usage this program will open a socket and wait for connections. +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 +Docker, this is port 5000 of the virtual machine that runs your Docker +images. You can find its IP address using the command ``docker-machine ip``. + +Unlike command line usage this program will open a socket and wait for +connections. .. warning:: - The OCRmyPDF web service wrapper is intended for demonstration or development. It provides no security, no authentication, no protection against denial of service attacks, and no load balancing. The default Flask WSGI server is used, which is intended for development only. The server is single-threaded and so can respond to only one client at a time. It cannot respond to clients while busy with OCR. + The OCRmyPDF web service wrapper is intended for demonstration or + development. It provides no security, no authentication, no + protection against denial of service attacks, and no load balancing. + The default Flask WSGI server is used, which is intended for + development only. The server is single-threaded and so can respond to + only one client at a time. While running OCR, it cannot respond to + any other clients. -Clients must keep their open connection while waiting for OCR to complete. This may entail setting a long timeout; this interface is more useful for internal HTTP API calls. +Clients must keep their open connection while waiting for OCR to +complete. This may entail setting a long timeout; this interface is more +useful for internal HTTP API calls. -Unlike the rest of OCRmyPDF, this web service is licensed under the Affero GPLv3 (AGPLv3) since Ghostscript, a dependency of OCRmyPDF, is also licensed in this way. +Unlike the rest of OCRmyPDF, this web service is licensed under the +Affero GPLv3 (AGPLv3) since Ghostscript is also licensed in this way. -In addition to the above, please read our :ref:`general remarks on using OCRmyPDF as a service `. - -Legacy Ubuntu Docker images ---------------------------- - -Previously OCRmyPDF was delivered in several Docker images for different purposes, based on Ubuntu. - -The Ubuntu-based images will be maintained for some time but should not be used for new deployments. They are as follows: - -.. list-table:: - :widths: auto - :header-rows: 1 - - * - Image name - - Download command - - Notes - * - ocrmypdf - - ``docker pull jbarlow83/ocrmypdf`` - - Latest ocrmypdf with Tesseract 4.0.0-beta1 on Ubuntu 18.04. Includes English, French, German, Spanish, Portugeuse and Simplified Chinese. - * - ocrmypdf-polyglot - - ``docker pull jbarlow83/ocrmypdf-polyglot`` - - As above, with all available language packs. - * - ocrmypdf-webservice - - ``docker pull jbarlow83/ocrmypdf-webservice`` - - All language packs, and a simple HTTP wrapper allowing OCRmyPDF to be used as a web service. Note that this component is licensed under AGPLv3. - -To execute the Ubuntu-based OCRmyPDF on a local file, you must `provide a writable volume to the Docker image `_, and both the input and output file must be inside the writable volume. This limitation applies only to the legacy images. - -This example command uses the current working directory as the writable volume: - -.. code-block:: bash - - docker run --rm -v "$(pwd):/home/docker" ocrmypdf - -In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``: - -.. code-block:: bash - - docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf - -.. note:: The working directory should be a writable local volume or Docker may not have permission to access it. - -Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after. - -In some environments the permissions associated with Docker can be complex to configure. The process that executes Docker may end up not having the permissions to write the specified file system. In that case one can stream the file into and out of the Docker process and avoid all permission hassles, using ``-`` as the input and output filename: - -.. code-block:: bash - - docker run --rm -i ocrmypdf - - output.pdf +In addition to the above, please read our +:ref:`general remarks on using OCRmyPDF as a service `. diff --git a/docs/errors.rst b/docs/errors.rst index 44bc5732..bf0b53d0 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -1,33 +1,53 @@ +===================== Common error messages ===================== Page already has text ---------------------- +===================== -.. code:: +.. code-block:: - ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR) + ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR) -You ran ocrmypdf on a file that already contains printable text or a hidden OCR text layer (it can't quite tell the difference). You probably don't want to do this, because the file is already searchable. +You ran ocrmypdf on a file that already contains printable text or a +hidden OCR text layer (it can't quite tell the difference). You probably +don't want to do this, because the file is already searchable. As the error message suggests, your options are: -- ``ocrmypdf --force-ocr`` to :ref:`rasterize ` all vector content and run OCR on the images. This is useful if a previous OCR program failed, or if the document contains a text watermark. - -- ``ocrmypdf --skip-text`` to skip OCR and other processing on any pages that contain text. Text pages will be copied into the output PDF without modification. +- ``ocrmypdf --force-ocr`` to :ref:`rasterize ` all + vector content and run OCR on the images. This is useful if a + previous OCR program failed, or if the document contains a text + watermark. +- ``ocrmypdf --skip-text`` to skip OCR and other processing on any + pages that contain text. Text pages will be copied into the output + PDF without modification. +- ``ocrmypdf --redo-ocr`` to scan the file for any existing OCR + (non-printing text), remove it, and do OCR again. This is one way + to take advantage of improvements in OCR accuracy. Printable vector + text is excluded from OCR, so this can be used on files that contain + a mix of digital and scanned files. Input file 'filename' is not a valid PDF ----------------------------------------- +======================================== -OCRmyPDF passes files through qpdf, a program that fixes errors in PDFs, before it tries to work on them. In most cases this happens because the PDF is corrupt and -truncated (incomplete file copying) and not much can be done. +OCRmyPDF checks files with pikepdf, a library that in turn uses libqpdf to fixes +errors in PDFs, before it tries to work on them. In most cases this happens +because the PDF is corrupt and truncated (incomplete file copying) and not much +can be done. -You can try rewriting the file with Ghostscript or pdftk: +You can try rewriting the file with Ghostscript: -- ``gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf`` +.. code-block:: bash -- ``pdftk input.pdf cat output output.pdf`` + gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf -Sometimes Acrobat can repair PDFs with its `Preflight tool `_. +``pdftk`` can also rewrite PDFs: +.. code-block:: bash + + pdftk input.pdf cat output output.pdf + +Sometimes Acrobat can repair PDFs with its `Preflight +tool `__. diff --git a/docs/images/logo-social.png b/docs/images/logo-social.png new file mode 100644 index 00000000..21354b9d Binary files /dev/null and b/docs/images/logo-social.png differ diff --git a/docs/images/logo.svg b/docs/images/logo.svg new file mode 100644 index 00000000..5d5c7a7f --- /dev/null +++ b/docs/images/logo.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index 3329eea8..91eac433 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,15 +1,12 @@ -.. ocrmypdf documentation master file, created by - sphinx-quickstart on Sun Sep 4 14:29:43 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - OCRmyPDF documentation ====================== -OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to -be searched. +OCRmyPDF adds an optical character recognition (OCR) text layer to scanned PDF +files, allowing them to be searched. -PDF is the best format for storing and exchanging scanned documents. Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs. +PDF is the best format for storing and exchanging scanned documents. +Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply +image processing and OCR to existing PDFs. .. toctree:: :maxdepth: 1 @@ -17,6 +14,7 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat introduction release_notes installation + optimizer languages jbig2 @@ -28,9 +26,18 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat docker advanced batch - security + performance + pdfsecurity errors +.. toctree:: + :caption: Developers + :maxdepth: 2 + + api + plugins + apiref + contributing Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst index 59f9d58e..6d096996 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,3 +1,4 @@ +=================== Installing OCRmyPDF =================== @@ -7,21 +8,38 @@ Installing OCRmyPDF |latest| The easiest way to install OCRmyPDF is to follow the steps for your operating -system/platform, although sometimes this version may be out of date. +system/platform. This version may be out of date, however. -If you want to use the latest version of OCRmyPDF, your best bet is to install -the most recent version your platform provides, and then upgrade that version by -installing the Python binary wheels. +These platforms have one-liner installs: + ++-------------------------------+-------------------------------+ +| Debian, Ubuntu | ``apt install ocrmypdf`` | ++-------------------------------+-------------------------------+ +| Windows Subsystem for Linux | ``apt install ocrmypdf`` | ++-------------------------------+-------------------------------+ +| Fedora | ``dnf install ocrmypdf`` | ++-------------------------------+-------------------------------+ +| macOS | ``brew install ocrmypdf`` | ++-------------------------------+-------------------------------+ +| LinuxBrew | ``brew install ocrmypdf`` | ++-------------------------------+-------------------------------+ +| FreeBSD | ``pkg install py37-ocrmypdf`` | ++-------------------------------+-------------------------------+ +| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` | ++-------------------------------+-------------------------------+ + +More detailed procedures are outlined below. If you want to do a manual +install, or install a more recent version than your platform provides, read on. .. contents:: Platform-specific steps :depth: 2 :local: Installing on Linux -------------------- +=================== -Debian and Ubuntu 16.10 or newer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Debian and Ubuntu 18.04 or newer +-------------------------------- .. |deb-stable| image:: https://repology.org/badge/version-for-repo/debian_stable/ocrmypdf.svg :alt: Debian 9 stable ("stretch") @@ -32,118 +50,178 @@ Debian and Ubuntu 16.10 or newer .. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg :alt: Debian unstable -.. |ubu-1710| image:: https://repology.org/badge/version-for-repo/ubuntu_17_10/ocrmypdf.svg - :alt: Ubuntu 17.10 - .. |ubu-1804| image:: https://repology.org/badge/version-for-repo/ubuntu_18_04/ocrmypdf.svg :alt: Ubuntu 18.04 LTS -.. |ubu-1810| image:: https://repology.org/badge/version-for-repo/ubuntu_18_10/ocrmypdf.svg - :alt: Ubuntu 18.10 +.. |ubu-2004| image:: https://repology.org/badge/version-for-repo/ubuntu_20_04/ocrmypdf.svg + :alt: Ubuntu 20.04 LTS +.. |ubu-2010| image:: https://repology.org/badge/version-for-repo/ubuntu_20_10/ocrmypdf.svg + :alt: Ubuntu 20.10 -+-------------------------------------------+ -| **OCRmyPDF versions in Debian & Ubuntu** | -+-------------------------------------------+ -| |latest| | -+-------------------------------------------+ -| |deb-stable| |deb-testing| |deb-unstable| | -+-------------------------------------------+ -| |ubu-1710| |ubu-1804| |ubu-1810| | -+-------------------------------------------+ ++-----------------------------------------------+ +| **OCRmyPDF versions in Debian & Ubuntu** | ++-----------------------------------------------+ +| |latest| | ++-----------------------------------------------+ +| |deb-stable| |deb-testing| |deb-unstable| | ++-----------------------------------------------+ +| |ubu-1804| |ubu-2004| |ubu-2010| | ++-----------------------------------------------+ -Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply +Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users +of Windows Subsystem for Linux, may simply .. code-block:: bash apt-get install ocrmypdf -As indicated in the table above, Debian and Ubuntu releases may lag behind the latest version. If the version available for your platform is out of date, you could opt to install the latest version from source. See `Installing HEAD revision from sources`_. +As indicated in the table above, Debian and Ubuntu releases may lag +behind the latest version. If the version available for your platform is +out of date, you could opt to install the latest version from source. +See `Installing HEAD revision from +sources <#installing-head-revision-from-sources>`__. Ubuntu 16.10 to 17.10 +inclusive also had ocrmypdf, but these versions are end of life. -For full details on version availability for your platform, check the `Debian Package Tracker `_ or `Ubuntu launchpad.net `_. +For full details on version availability for your platform, check the +`Debian Package Tracker `__ or +`Ubuntu launchpad.net `__. .. note:: - OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder. OCRmyPDF works fine without it but will produce larger output files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically detect it (specifically the ``jbig2`` binary) on the ``PATH``. To add JBIG2 encoding, see :ref:`jbig2`. + OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder. + OCRmyPDF works fine without it but will produce larger output files. + If you build jbig2enc from source, ocrmypdf 7.0.0 and later will + automatically detect it (specifically the ``jbig2`` binary) on the + ``PATH``. To add JBIG2 encoding, see :ref:`jbig2`. -Fedora 29 or newer -^^^^^^^^^^^^^^^^^^ +Fedora +------ -.. |fedora-29| image:: https://repology.org/badge/version-for-repo/fedora29/ocrmypdf.svg - :alt: Fedora 29 +.. |fedora-32| image:: https://repology.org/badge/version-for-repo/fedora_32/ocrmypdf.svg + :alt: Fedora 32 + +.. |fedora-33| image:: https://repology.org/badge/version-for-repo/fedora_33/ocrmypdf.svg + :alt: Fedora 33 .. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg :alt: Fedore Rawhide ++-----------------------------------------------+ +| **OCRmyPDF version** | ++-----------------------------------------------+ +| |latest| | ++-----------------------------------------------+ +| |fedora-32| |fedora-33| |fedora-rawhide| | ++-----------------------------------------------+ -+------------------------------+ -| **OCRmyPDF version** | -+------------------------------+ -| |latest| | -+------------------------------+ -| |fedora-29| |fedora-rawhide| | -+------------------------------+ - -Users of Fedora 29 later may simply +Users of Fedora 29 or later may simply .. code-block:: bash dnf install ocrmypdf -For full details on version availability, check the `Fedora Package Tracker -`_. +For full details on version availability, check the `Fedora Package +Tracker `__. -If the version available for your platform is out of date, you could opt to -install the latest version from source. See `Installing HEAD revision from -sources`_. +If the version available for your platform is out of date, you could opt +to install the latest version from source. See `Installing HEAD revision +from sources <#installing-head-revision-from-sources>`__. .. note:: - OCRmyPDF for Fedora currently omits the JBIG2 encoder due to patent issues. - OCRmyPDF works fine without it but will produce larger output files. If you - build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically - detect it on the ``PATH``. To add JBIG2 encoding, see `Installing the JBIG2 - encoder `_. + OCRmyPDF for Fedora currently omits the JBIG2 encoder due to patent + issues. OCRmyPDF works fine without it but will produce larger output + files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later + will automatically detect it on the ``PATH``. To add JBIG2 encoding, + see `Installing the JBIG2 encoder `__. -Installing the latest version on Ubuntu 18.04 LTS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _ubuntu-lts-latest: -Ubuntu 18.04 includes ocrmypdf 6.1.2. To install a more recent version, first -install the system version to get most of the dependencies: +Installing the latest version on Ubuntu 20.04 LTS +------------------------------------------------- + +Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To +install a more recent version, uninstall the system-provided version of +ocrmypdf, and install the following dependencies: .. code-block:: bash - sudo apt-get update - sudo apt-get install \ - ocrmypdf \ - python3-pip + sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed + sudo apt-get -y update + sudo apt-get -y install \ + ghostscript \ + icc-profiles-free \ + liblept5 \ + libxml2 \ + pngquant \ + python3-pip \ + tesseract-ocr \ + zlib1g -There are a few dependency changes between ocrmypdf 6.1.2 and 7.x. Let's get -these, too. +To install ocrmypdf for the system: .. code-block:: bash - sudo apt-get install \ - libexempi3 \ - pngquant + pip3 install ocrmypdf -Then install the most recent ocrmypdf for the local user and set the user's ``PATH`` to check for the user's Python packages. +To install for the current user only: .. code-block:: bash export PATH=$HOME/.local/bin:$PATH pip3 install --user ocrmypdf +Ubuntu 18.04 LTS +---------------- + +Ubuntu 18.04 includes ocrmypdf 6.1.2 - you can install that with ``apt``, but +it is quite old now. To install a more recent version, uninstall the old version +of ocrmypdf, and install the following dependencies: + +.. code-block:: bash + + sudo apt-get -y remove ocrmypdf + sudo apt-get -y update + sudo apt-get -y install \ + ghostscript \ + icc-profiles-free \ + liblept5 \ + libxml2 \ + pngquant \ + python3-cffi \ + python3-distutils \ + python3-pkg-resources \ + python3-reportlab \ + qpdf \ + tesseract-ocr \ + zlib1g \ + unpaper + +We will need a newer version of ``pip`` then was available for Ubuntu 18.04: + +.. code-block:: bash + + wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py + +Then install the most recent ocrmypdf for the local user and set the +user's ``PATH`` to check for the user's Python packages. + +.. code-block:: bash + + export PATH=$HOME/.local/bin:$PATH + python3 -m pip install --user ocrmypdf + To add JBIG2 encoding, see :ref:`jbig2`. Ubuntu 16.04 LTS -^^^^^^^^^^^^^^^^ +---------------- -No package is available for Ubuntu 16.04. OCRmyPDF 8.0 and newer require Python -3.6. Ubuntu 16.04 ships Python 3.5, but you can install Python 3.6 on it. Or, -you can skip Python 3.6 and install OCRmyPDF 7.x or older - for that procedure, -please see the installation documentation for the version of OCRmyPDF you plan -to use. +No package is available for Ubuntu 16.04. OCRmyPDF 8.0 and newer require +Python 3.6. Ubuntu 16.04 ships Python 3.5, but you can install Python +3.6 on it. Or, you can skip Python 3.6 and install OCRmyPDF 7.x or older +- for that procedure, please see the installation documentation for the +version of OCRmyPDF you plan to use. **Install system packages for OCRmyPDF** @@ -165,13 +243,13 @@ to use. tesseract-ocr \ unpaper -This will install a Python 3.6 binary at ``/usr/bin/python3.6`` alongside the -system's Python 3.5. Do not remove the system Python. This will also install -Tesseract 4.0 from a PPA, since the version available in Ubuntu 16.04 is too old -for OCRmyPDF. +This will install a Python 3.6 binary at ``/usr/bin/python3.6`` +alongside the system's Python 3.5. Do not remove the system Python. This +will also install Tesseract 4.0 from a PPA, since the version available +in Ubuntu 16.04 is too old for OCRmyPDF. -Now install pip for Python 3.6. This will install the Python 3.6 version of -``pip`` at ``/usr/local/bin/pip``. +Now install pip for Python 3.6. This will install the Python 3.6 version +of ``pip`` at ``/usr/local/bin/pip``. .. code-block:: bash @@ -179,8 +257,9 @@ Now install pip for Python 3.6. This will install the Python 3.6 version of **Install OCRmyPDF** -OCRmyPDF requires the locale to be set for UTF-8. **On some minimal Ubuntu -installations systems**, it may be necessary to set the locale. +OCRmyPDF requires the locale to be set for UTF-8. **On some minimal +Ubuntu installations**, such as the Ubuntu 16.04 Docker images it may be +necessary to set the locale. .. code-block:: bash @@ -194,111 +273,161 @@ environment variable contains ``$HOME/.local/bin``. .. code-block:: bash export PATH=$HOME/.local/bin:$PATH - pip3 install --user ocrmypdf + pip3.6 install --user ocrmypdf To add JBIG2 encoding, see :ref:`jbig2`. -Ubuntu 14.04 LTS -^^^^^^^^^^^^^^^^ - -Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other -options, because of its age. Several backports are required. For explanations of -some steps of this procedure, see the similar steps for Ubuntu 16.04. - -Install system dependencies: - -.. code-block:: bash - - sudo apt-get update - sudo apt-get install \ - software-properties-common python-software-properties \ - zlib1g-dev \ - libexempi3 \ - libjpeg-dev \ - libffi-dev \ - pngquant \ - qpdf - -We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1), -Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and -Tesseract 3.x on your system. Python 3.6 will be installed alongside the system -Python 3.4. - -If you prefer to not modify your system in this matter, consider using a Docker -container. - -.. code-block:: bash - - sudo add-apt-repository ppa:vshn/ghostscript -y - sudo add-apt-repository ppa:heyarje/libav-11 -y - sudo add-apt-repository ppa:alex-p/tesseract-ocr -y - sudo add-apt-repository ppa:jonathonf/python-3.6 -y - - sudo apt-get update - - sudo apt-get install \ - python3.6-dev \ - ghostscript \ - tesseract-ocr \ - tesseract-ocr-eng \ - libavformat56 libavcodec56 libavutil54 \ - wget - -Now we need to install ``pip`` and let it install ocrmypdf: - -.. code-block:: bash - - curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip - pip3.6 install ocrmypdf - -These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only): - -.. code-block:: bash - - wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O unpaper_6.1-1.deb - sudo dpkg -i unpaper_6.1-1.deb - -To add JBIG2 encoding, see :ref:`jbig2`. - -ArchLinux (AUR) -^^^^^^^^^^^^^^^ +Arch Linux (AUR) +---------------- .. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg :alt: ArchLinux :target: https://repology.org/metapackage/ocrmypdf -There is an `ArchLinux User Repository package for ocrmypdf `_. You can use the following command. +There is an `Arch User Repository (AUR) package for OCRmyPDF +`__. + +Installing AUR packages as root is not allowed, so you must first `setup a +non-root user +`__ and +`configure sudo `__. +The standard Docker image, ``archlinux/base:latest``, does **not** have a +non-root user configured, so users of that image must follow these guides. If +you are using a VM image, such as `the official Vagrant image +`__, this work may already +be completed for you. + +Next you should install the `base-devel package group +`__. This includes the +standard tooling needed to build packages, such as a compiler and binary tools. .. code-block:: bash - yaourt -S ocrmypdf + sudo pacman -S base-devel -If you have any difficulties with installation, check the repository package page. +Now you are ready to install the OCRmyPDF package. + +.. code-block:: bash + + curl -O https://aur.archlinux.org/cgit/aur.git/snapshot/ocrmypdf.tar.gz + tar xvzf ocrmypdf.tar.gz + cd ocrmypdf + makepkg -sri + +At this point you will have a working install of OCRmyPDF, but the Tesseract +install won’t include any OCR language data. You can install `the +tesseract-data package group +`__ to add all supported +languages, or use that package listing to identify the appropriate package for +your desired language. + +.. code-block:: bash + + sudo pacman -S tesseract-data-eng + +As an alternative to this manual procedure, consider using an `AUR helper +`__. Such a tool will +automatically fetch, build and install the AUR package, resolve dependencies +(including dependencies on AUR packages), and ease the upgrade procedure. + +If you have any difficulties with installation, check the repository package +page. + +.. note:: + + The OCRmyPDF AUR package currently omits the JBIG2 encoder. OCRmyPDF works + fine without it but will produce larger output files. The encoder is + available from `the jbig2enc-git AUR package + `__ and may be installed + using the same series of steps as for the installation OCRmyPDF AUR + package. Alternatively, it may be built manually from source following the + instructions in `Installing the JBIG2 encoder `__. If JBIG2 is + installed, OCRmyPDF 7.0.0 and later will automatically detect it. + +Alpine Linux +------------ + +.. image:: https://repology.org/badge/version-for-repo/alpine_edge/ocrmypdf.svg + :alt: Alpine Linux + :target: https://repology.org/metapackage/ocrmypdf + +To install OCRmyPDF for Alpine Linux: + +.. code-block:: bash + + apk add ocrmypdf + +Mageia 7 +-------- + +There is no OS-level packaging available for Mageia, so you must install the +dependencies: + +.. code-block:: bash + + # As root user + urpmi.update -a + urpmi \ + ghostscript \ + icc-profiles-openicc \ + jbig2dec \ + lib64leptonica5 \ + pngquant \ + python3-pip \ + python3-cffi \ + python3-distutils-extra \ + python3-pkg-resources \ + python3-reportlab \ + qpdf \ + tesseract \ + tesseract-osd \ + tesseract-eng \ + tesseract-fra + +To install ocrmypdf for the system: + +.. code-block:: bash + + # As root user + pip3 install ocrmypdf + ldconfig + +Or, to install for the current user only: + +.. code-block:: bash + + export PATH=$HOME/.local/bin:$PATH + pip3 install --user ocrmypdf Other Linux packages -^^^^^^^^^^^^^^^^^^^^ +-------------------- -See the `Repology `_ page. +See the +`Repology `__ page. -In general, first install the OCRmyPDF package for your system, then optionally use the procedure `Installing with Python pip`_ to install a more recent version. +In general, first install the OCRmyPDF package for your system, then +optionally use the procedure `Installing with Python +pip <#installing-with-python-pip>`__ to install a more recent version. Installing on macOS -------------------- +=================== Homebrew -^^^^^^^^ +-------- .. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg :alt: homebrew :target: http://brewformulas.org/Ocrmypdf -OCRmyPDF is now a standard `Homebrew `_ formula. To install on macOS: +OCRmyPDF is now a standard `Homebrew `__ formula. To +install on macOS: .. code-block:: bash brew install ocrmypdf -This will include only the English language pack. If you need other languages you can optionally install them all: +This will include only the English language pack. If you need other +languages you can optionally install them all: .. code-block:: bash @@ -306,18 +435,26 @@ This will include only the English language pack. If you need other languages yo .. note:: - Users who previously installed OCRmyPDF on macOS using ``pip install ocrmypdf`` should remove the pip version (``pip3 uninstall ocrmypdf``) before switching to the Homebrew version. + Users who previously installed OCRmyPDF on macOS using + ``pip install ocrmypdf`` should remove the pip version + (``pip3 uninstall ocrmypdf``) before switching to the Homebrew + version. .. note:: - Users who previously installed OCRmyPDF from the private tap should switch to the mainline version (``brew untap jbarlow83/ocrmypdf``) and install from there. + Users who previously installed OCRmyPDF from the private tap should + switch to the mainline version (``brew untap jbarlow83/ocrmypdf``) + and install from there. Manual installation on macOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------- -These instructions probably work on all macOS supported by Homebrew. +These instructions probably work on all macOS supported by Homebrew, and are +for installing a more current version of OCRmyPDF than is available from +Homebrew. Note that the Homebrew versions usually track the release versions +fairly closely. -If it's not already present, `install Homebrew `_. +If it's not already present, `install Homebrew `__. Update Homebrew: @@ -325,20 +462,18 @@ Update Homebrew: brew update -Install or upgrade the required Homebrew packages, if any are missing. To do this, download the ``Brewfile`` that lists all of the dependencies to the current directory, and run ``brew bundle`` to process them (installing or upgrading as needed). ``Brewfile`` is a plain text file. +Install or upgrade the required Homebrew packages, if any are missing. +To do this, use ``brew edit ocrmypdf`` to obtain a recent list of Homebrew +dependencies. You could also check the ``.workflows/build.yml``. -.. code-block:: bash - - wget https://github.com/jbarlow83/OCRmyPDF/raw/master/.travis/Brewfile - brew bundle - -This will include the English, French, German and Spanish language packs. If you need other languages you can optionally install them all: +This will include the English, French, German and Spanish language +packs. If you need other languages you can optionally install them all: .. _macos-all-languages: -.. code-block:: bash + .. code-block:: bash - brew install tesseract --with-all-languages # Option 2: for all language packs + brew install tesseract-lang # Option 2: for all language packs Update the homebrew pip: @@ -364,94 +499,285 @@ The command line program should now be available: ocrmypdf --help -Installing the Docker image +Installing on Windows +===================== + +Native Windows +-------------- + +.. note:: + + Administrator privileges will be required for some of these steps. + +You must install the following for Windows: + +* Python 3.7 (64-bit) or later +* Tesseract 4.0 or later +* Ghostscript 9.50 or later + +Using the `Chocolatey `_ package manager, install the +following when running in an Administrator command prompt: + +* ``choco install python3`` +* ``choco install --pre tesseract`` +* ``choco install ghostscript`` +* ``choco install pngquant`` (optional) + +The commands above will install Python 3.x (latest version), Tesseract, Ghostscript +and pngquant. Chocolatey may also need to install the Windows Visual C++ Runtime +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 +Administrator.): + +* ``pip install ocrmypdf`` + +Chocolatey automatically selects appropriate versions of these applications. If you +are installing them manually, please install 64-bit versions of all applications for +64-bit Windows, or 32-bit versions of all applications for 32-bit Windows. Mixing +the "bitness" of these programs will lead to errors. + +OCRmyPDF will check the Windows Registry and standard locations in your Program Files +for third party software it needs (specifically, Tesseract and Ghostscript). To +override the versions OCRmyPDF selects, you can modify the ``PATH`` environment +variable. `Follow these directions `_ +to change the PATH. + +.. warning:: + + 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. + Please download Python from Python.org or Chocolatey instead, and do not use the + Microsoft Store version. + +Windows Subsystem for Linux --------------------------- -For some users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option. +#. Install Ubuntu 18.04 for Windows Subsystem for Linux, if not already installed. +#. Follow the procedure to install :ref:`OCRmyPDF on Ubuntu 18.04 `. +#. Open the Windows command prompt and create a symlink: -See `OCRmyPDF Docker Image `_ for more information. +.. code-block:: powershell -Installing on Windows ---------------------- + wsl sudo ln -s /home/$USER/.local/bin/ocrmypdf /usr/local/bin/ocrmypdf -Direct installation on Windows is not possible. `Install the Docker `_ container as described above. Ensure that your command prompt can run the docker "hello world" container. +Then confirm that the expected version from PyPI (|latest|) is installed: -It would probably not be too difficult to port on Windows. The main reason this has been avoided is the difficulty of packaging and installing the various non-Python dependencies: Tesseract, QPDF, Ghostscript, Leptonica. Pull requests to add or improve Windows support would be quite welcome. +.. code-block:: powershell + + wsl ocrmypdf --version + +You can then run OCRmyPDF in the Windows command prompt or Powershell, prefixing +``wsl``, and call it from Windows programs or batch files. + +Cygwin64 +-------- + +First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``:: + + python36 (or later) + python3?-devel + python3?-pip + python3?-lxml + python3?-imaging + + (where 3? means match the version of python3 you installed) + + gcc-g++ + ghostscript (<=9.50 or >=9.52-2 see note below) + libexempi3 + libexempi-devel + libffi6 + libffi-devel + pngquant + qpdf + libqpdf-devel + tesseract-ocr + 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 +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 +``pip`` (with, for instance ``pip3 install --upgrade pip``) the the command is +likely just ``pip`` instead of ``pip3``: + +.. code-block:: bash + + pip3 install wheel + pip3 install ocrmypdf + +The optional dependency "unpaper" that is currently not available under Cygwin. +Without it, certain options such as ``--clean`` will produce an error message. +However, the OCR-to-text-layer functionality is available. + +Docker +------ + +You can also :ref:`Install the Docker ` container on Windows. Ensure that +your command prompt can run the docker "hello world" container. + +Installing on FreeBSD +===================== + +.. image:: https://repology.org/badge/version-for-repo/freebsd/python:ocrmypdf.svg + :alt: FreeBSD + :target: https://repology.org/project/python:ocrmypdf/versions + +FreeBSD 11.3, 12.0, 12.1-RELEASE and 13.0-CURRENT are supported. Other +versions likely work but have not been tested. + +.. code-block:: bash + + pkg install py37-ocrmypdf + +To install a more recent version, you could attempt to first install the system +version with ``pkg``, then use ``pip install --user ocrmypdf``. + +Installing the Docker image +=========================== + +For some users, installing the Docker image will be easier than +installing all of OCRmyPDF's dependencies. + +See :ref:`docker` for more information. Installing with Python pip --------------------------- +========================== -OCRmyPDF is delivered by PyPI because it is a convenient way to install the latest version. However, PyPI and ``pip`` cannot address the fact that ``ocrmypdf`` depends on certain non-Python system libraries and programs being instsalled. +OCRmyPDF is delivered by PyPI because it is a convenient way to install +the latest version. However, PyPI and ``pip`` cannot address the fact +that ``ocrmypdf`` depends on certain non-Python system libraries and +programs being installed. -For best results, first install `your platform's version `_ of ``ocrmypdf``, using the instructions elsewhere in this document. Then you can use ``pip`` to get the latest version if your platform version is out of date. Chances are that this will satisfy most dependencies. +.. 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 inforation on Debian-Python issues + `__. + +For best results, first install `your platform's +version `__ of +``ocrmypdf``, using the instructions elsewhere in this document. Then +you can use ``pip`` to get the latest version if your platform version +is out of date. Chances are that this will satisfy most dependencies. Use ``ocrmypdf --version`` to confirm what version was installed. -Then you can install the latest OCRmyPDF from the Python wheels. First try: +Then you can install the latest OCRmyPDF from the Python wheels. First +try: .. code-block:: bash pip3 install --user ocrmypdf -You should then be able to run ``ocrmypdf --version`` and see that the latest version was located. +You should then be able to run ``ocrmypdf --version`` and see that the +latest version was located. -Since ``pip3 install --user`` does not work correctly on some platforms, notably Ubuntu 16.04 and older, and the Homebrew version of Python, instead use this for a system wide installation: +Since ``pip3 install --user`` does not work correctly on some platforms, +notably Ubuntu 16.04 and older, and the Homebrew version of Python, +instead use this for a system wide installation: .. code-block:: bash pip3 install ocrmypdf +.. 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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------------- -OCRmyPDF currently requires these external programs and libraries to be installed, and must be satisfied using the operating system package manager. ``pip`` cannot provide them. +OCRmyPDF currently requires these external programs and libraries to be +installed, and must be satisfied using the operating system package +manager. ``pip`` cannot provide them. -- Python 3.6 or newer -- Ghostscript 9.15 or newer -- qpdf 8.1.0 or newer -- Tesseract 4.0.0-alpha or newer +- Python 3.6 or newer +- Ghostscript 9.15 or newer +- qpdf 8.1.0 or newer +- Tesseract 4.0.0-beta or newer As of ocrmypdf 7.2.1, the following versions are recommended: -- Python 3.7 -- Ghostscript 9.23 or newer -- qpdf 8.2.1 -- Tesseract 4.0.0 or newer -- jbig2enc 0.29 or newer -- pngquant 2.5 or newer -- unpaper 6.1 +- Python 3.7 or 3.8 +- Ghostscript 9.23 or newer +- qpdf 8.2.1 +- Tesseract 4.0.0 or newer +- jbig2enc 0.29 or newer +- pngquant 2.5 or newer +- unpaper 6.1 -jbig2enc, pngquant, and unpaper are optional. If missing certain features are disabled. OCRmyPDF will discover them as soon as they are available. +jbig2enc, pngquant, and unpaper are optional. If missing certain +features are disabled. OCRmyPDF will discover them as soon as they are +available. -**jbig2enc**, if present, will be used to optimize the encoding of monochrome images. This can significantly reduce the file size of the output file. It is not required. `jbig2enc `_ is not generally available for Ubuntu or Debian due to lingering concerns about patent issues, but can easily be built from source. To add JBIG2 encoding, see :ref:`jbig2`. +**jbig2enc**, if present, will be used to optimize the encoding of +monochrome images. This can significantly reduce the file size of the +output file. It is not required. +`jbig2enc `__ is not generally +available for Ubuntu or Debian due to lingering concerns about patent +issues, but can easily be built from source. To add JBIG2 encoding, see +:ref:`jbig2`. -**pngquant**, if present, is optionally used to optimize the encoding of PNG-style images in PDFs (actually, any that are that losslessly encoded) by lossily quantizing to a smaller color palette. It is only activated then the ``--optimize`` argument is ``2`` or ``3``. +**pngquant**, if present, is optionally used to optimize the encoding of +PNG-style images in PDFs (actually, any that are that losslessly +encoded) by lossily quantizing to a smaller color palette. It is only +activated then the ``--optimize`` argument is ``2`` or ``3``. -**unpaper**, if present, enables the ``--clean`` and ``--clean-final`` command line options. - -These are in addition to the Python packaging dependencies, meaning that unfortunately, the ``pip install`` command cannot satisfy all of them. +**unpaper**, if present, enables the ``--clean`` and ``--clean-final`` +command line options. +These are in addition to the Python packaging dependencies, meaning that +unfortunately, the ``pip install`` command cannot satisfy all of them. Installing HEAD revision from sources -------------------------------------- +===================================== -If you have ``git`` and Python 3.6 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing. +If you have ``git`` and Python 3.6 or newer installed, you can install +from source. When the ``pip`` installer runs, it will alert you if +dependencies are missing. -If you prefer to build every from source, you will need to `build pikepdf from source `_. First ensure you can build and install pikepdf. +If you prefer to build every from source, you will need to `build +pikepdf from +source `__. +First ensure you can build and install pikepdf. -To install the HEAD revision from sources in the current Python 3 environment: +To install the HEAD revision from sources in the current Python 3 +environment: .. code-block:: bash pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git -Or, to install in `development mode `_, allowing customization of OCRmyPDF, use the ``-e`` flag: +Or, to install in `development +mode `__, +allowing customization of OCRmyPDF, use the ``-e`` flag: .. code-block:: bash pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git -You may find it easiest to install in a virtual environment, rather than system-wide: +You may find it easiest to install in a virtual environment, rather than +system-wide: .. code-block:: bash @@ -461,8 +787,8 @@ You may find it easiest to install in a virtual environment, rather than system- cd OCRmyPDF pip3 install . -However, ``ocrmypdf`` will only be accessible on the system PATH -when you activate the virtual environment. +However, ``ocrmypdf`` will only be accessible on the system PATH when +you activate the virtual environment. To run the program: @@ -476,7 +802,7 @@ dependencies. Older version than the ones mentioned in the release notes are likely not to be compatible to OCRmyPDF. For development -^^^^^^^^^^^^^^^ +--------------- To install all of the development and test requirements: @@ -492,15 +818,17 @@ To install all of the development and test requirements: To add JBIG2 encoding, see :ref:`jbig2`. Shell completions ------------------ +================= Completions for ``bash`` and ``fish`` are available in the project's ``misc/completion`` folder. The ``bash`` completions are likely ``zsh`` -compatible but this has not been confirmed. Package maintainers, please install -these at the appropriate locations for your system. +compatible but this has not been confirmed. Package maintainers, please +install these at the appropriate locations for your system. -To manually install the ``bash`` completion, copy ``misc/completion/ocrmypdf.bash`` to -``/etc/bash_completion.d/ocrmypdf`` (rename the file). +To manually install the ``bash`` completion, copy +``misc/completion/ocrmypdf.bash`` to ``/etc/bash_completion.d/ocrmypdf`` +(rename the file). -To manually install the ``fish`` completion, copy ``misc/completion/ocrmypdf.fish`` to +To manually install the ``fish`` completion, copy +``misc/completion/ocrmypdf.fish`` to ``~/.config/fish/completions/ocrmypdf.fish``. diff --git a/docs/introduction.rst b/docs/introduction.rst index f7f80568..360d57ec 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -1,119 +1,233 @@ +============ Introduction ============ -OCRmyPDF is a Python 3 package that adds OCR layers to PDFs. + +OCRmyPDF is a Python 3 application and library that adds OCR layers to PDFs. About OCR ---------- +========= -`Optical character recognition `_ is technology that converts images of typed or handwritten text, such as in a scanned document, to computer text that can be searched and copied. +`Optical character +recognition `__ +is technology that converts images of typed or handwritten text, such as +in a scanned document, to computer text that can be selected, searched and copied. -OCRmyPDF uses `Tesseract `_, the best available open source OCR engine, to perform OCR. +OCRmyPDF uses +`Tesseract `__, the best +available open source OCR engine, to perform OCR. .. _raster-vector: About PDFs ----------- +========== -PDFs are page description files that attempts to preserve a layout exactly. They contain `vector graphics `_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents. +PDFs are page description files that attempts to preserve a layout +exactly. They contain `vector +graphics `__ +that can contain raster objects such as scanned images. Because PDFs can +contain multiple pages (unlike many image formats) and can contain fonts +and text, it is a good formats for exchanging scanned documents. -.. image:: images/bitmap_vs_svg.svg +|image| -A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page. - -Rasterizing a PDF is the process of generating an image suitable for display or analyzing with an OCR engine. OCR engines like Tesseract work with images, not vector objects. +A PDF page might contain multiple images, even if it only appears to +have one image. Some scanners or scanning software will segment pages +into monochromatic text and color regions for example, to improve the +compression ratio and appearance of the page. +Rasterizing a PDF is the process of generating an image suitable for +display or analyzing with an OCR engine. OCR engines like Tesseract work +with images, not vector objects. About PDF/A ------------ +=========== -`PDF/A `_ is an ISO-standardized subset of the full PDF specification that is designed for archiving (the 'A' stands for Archive). PDF/A differs from PDF primarily by omitting features that would make it difficult to read the file in the future, such as embedded Javascript, video, audio and references to external fonts. All fonts and resources needed to interpret the PDF must be contained within it. Because PDF/A disables Javascript and other types of embedded content, it is probably more secure. +`PDF/A `__ is an ISO-standardized +subset of the full PDF specification that is designed for archiving (the +'A' stands for Archive). PDF/A differs from PDF primarily by omitting +features that would make it difficult to read the file in the future, +such as embedded Javascript, video, audio and references to external +fonts. All fonts and resources needed to interpret the PDF must be +contained within it. Because PDF/A disables Javascript and other types +of embedded content, it is probably more secure. There are various conformance levels and versions, such as "PDF/A-2b". -Generally speaking, the best format for scanned documents is PDF/A. Some governments and jurisdictions, US Courts in particular, `mandate the use of PDF/A `_ for scanned documents. +Generally speaking, the best format for scanned documents is PDF/A. Some +governments and jurisdictions, US Courts in particular, `mandate the use +of PDF/A `__ for scanned +documents. -Since most people who scan documents are interested in reading them indefinitely into the future, OCRmyPDF generates PDF/A-2b by default. - -PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a PDF/A, which may confuse some users. It also tends to produce larger files than PDF, because it embeds certain resources even if they are commonly available. PDF/A files can be digitally signed, but may not be encrypted, to ensure they can be read in the future. Fortunately, converting from PDF/A to a regular PDF is trivial, and any PDF viewer can view PDF/A. +Since most people who scan documents are interested in reading them +indefinitely into the future, OCRmyPDF generates PDF/A-2b by default. +PDF/A has a few drawbacks. Some PDF viewers include an alert that the +file is a PDF/A, which may confuse some users. It also tends to produce +larger files than PDF, because it embeds certain resources even if they +are commonly available. PDF/A files can be digitally signed, but may not +be encrypted, to ensure they can be read in the future. Fortunately, +converting from PDF/A to a regular PDF is trivial, and any PDF viewer +can view PDF/A. What OCRmyPDF does ------------------- +================== -OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript `_ to rasterize the page, and then performs on OCR on the rasterized image to create an OCR "layer". The layer is then grafted back onto the original PDF. +OCRmyPDF analyzes each page of a PDF to determine the colorspace and +resolution (DPI) needed to capture all of the information on that page +without losing content. It uses +`Ghostscript `__ to rasterize the page, and +then performs on OCR on the rasterized image to create an OCR "layer". +The layer is then grafted back onto the original PDF. -While one can use a program like Ghostscript or ImageMagick to get an image and put the image through Tesseract, that actually creates a new PDF and many details may be lost. OCRmyPDF can produce a minimally changed PDF as output. +While one can use a program like Ghostscript or ImageMagick to get an +image and put the image through Tesseract, that actually creates a new +PDF and many details may be lost. OCRmyPDF can produce a minimally +changed PDF as output. -OCRmyPDF also some image processing options like deskew which improve the appearance of files and quality of OCR. When these are used, the OCR layer is grafted onto the processed image instead. - -By default, OCRmyPDF produces archival PDFs – PDF/A, which are a stricter subset of PDF features designed for long term archives. If regular PDFs are desired, this can be disabled with ``--output-type pdf``. +OCRmyPDF also some image processing options like deskew which improve +the appearance of files and quality of OCR. When these are used, the OCR +layer is grafted onto the processed image instead. +By default, OCRmyPDF produces archival PDFs – PDF/A, which are a +stricter subset of PDF features designed for long term archives. If +regular PDFs are desired, this can be disabled with +``--output-type pdf``. Why you shouldn't do this manually ----------------------------------- +================================== -A PDF is similar to an HTML file, in that it contains document structure along with images. Sometimes a PDF does nothing more than present a full page image, but often there is additional content that would be lost. +A PDF is similar to an HTML file, in that it contains document structure +along with images. Sometimes a PDF does nothing more than present a full +page image, but often there is additional content that would be lost. A manual process could work like either of these: -1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the layout of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.). +1. Rasterize each page as an image, OCR the images, and combine the + output into a PDF. This preserves the layout of each page, but + resamples all images (possibly losing quality, increasing file size, + introducing compression artifacts, etc.). +2. Extract each image, OCR, and combine the output into a PDF. This + loses the context in which images are used in the PDF, meaning that + cropping, rotation and scaling of pages may be lost. Some scanned + PDFs use multiple images segmented into black and white, grayscale + and color regions, with stencil masks to prevent overlap, as this can + enhance the appearance of a file while reducing file size. Clearly, + reassembling these images will be easy. This also loses and text or + vector art on any pages in a PDF with both scanned and pure digital + content. -2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some scanned PDFs use multiple images segmented into black and white, grayscale and color regions, with stencil masks to prevent overlap, as this can enhance the appearance of a file while reducing file size. Clearly, reassembling these images will be easy. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content. +In the case of a PDF that is nothing other than a container of images +(no rotation, scaling, cropping, one image per page), the second +approach can be lossless. -In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach can be lossless. - -OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible. - -OCRmyPDF also supports a many, many edge cases that have cropped over several years of development. We support PDF features like images inside of Form XObjects, and pages with UserUnit scaling. We support rare image formats like non-monochrome 1-bit images. We warn about files you may not to OCR. Thanks to pikepdf and QPDF, we auto-repair PDFs that are damaged. (Not that you need to know what any of these are! You should be able to throw any PDF at it.) +OCRmyPDF uses several strategies depending on input options and the +input PDF itself, but generally speaking it rasterizes a page for OCR +and then grafts the OCR back onto the original. As such it can handle +complex PDFs and still preserve their contents as much as possible. +OCRmyPDF also supports a many, many edge cases that have cropped over +several years of development. We support PDF features like images inside +of Form XObjects, and pages with UserUnit scaling. We support rare image +formats like non-monochrome 1-bit images. We warn about files you may +not to OCR. Thanks to pikepdf and QPDF, we auto-repair PDFs that are +damaged. (Not that you need to know what any of these are! You should be +able to throw any PDF at it.) Limitations ------------ +=========== -OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these limitations, as do any other programs that rely on Tesseract: +OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences +these limitations, as do any other programs that rely on Tesseract: -* The OCR is not as accurate as commercial solutions such as Abbyy. -* It is not capable of recognizing handwriting. -* It may find gibberish and report this as OCR output. -* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor. -* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns, and may try to join text across columns. -* Poor quality scans may produce poor quality OCR. Garbage in, garbage out. -* It does not expose information about what font family text belongs to. +- The OCR is not as accurate as commercial solutions such as Abbyy. +- It is not capable of recognizing handwriting. +- It may find gibberish and report this as OCR output. +- If a document contains languages outside of those given in the + ``-l LANG`` arguments, results may be poor. +- It is not always good at analyzing the natural reading order of + documents. For example, it may fail to recognize that a document + contains two columns, and may try to join text across columns. +- Poor quality scans may produce poor quality OCR. Garbage in, garbage + out. +- It does not expose information about what font family text belongs + to. OCRmyPDF is also limited by the PDF specification: -* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others. -* Because some popular open source PDF viewers have a particularly hard time with spaces betweem words, OCRmyPDF appends a space to each text element as a workaround (when using ``--pdf-renderer hocr``). While this mixes document structure with graphical information that ideally should be left to the PDF viewer to interpret, it improves compatibility with some viewers and does not cause problems for better ones. +- PDF encodes the position of text glyphs but does not encode document + structure. There is no markup that divides a document in sections, + paragraphs, sentences, or even words (since blank spaces are not + represented). As such all elements of document structure including + the spaces between words must be derived heuristically. Some PDF + viewers do a better job of this than others. +- Because some popular open source PDF viewers have a particularly hard + time with spaces between words, OCRmyPDF appends a space to each text + element as a workaround (when using ``--pdf-renderer hocr``). While + this mixes document structure with graphical information that ideally + should be left to the PDF viewer to interpret, it improves + compatibility with some viewers and does not cause problems for + better ones. Ghostscript also imposes some limitations: -* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled. -* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled. -* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format. (Ghostscript 9.25+ can copy JPEG images without transcoding them; earlier versions will transcode.) -* Ghostscript's PDF/A conversion removes any XMP metadata that is not one of the standard XMP metadata namespaces for PDFs. In particular, PRISM Metdata is removed. +- PDFs containing JBIG2-encoded content will be converted to CCITT + Group4 encoding, which has lower compression ratios, if Ghostscript + PDF/A is enabled. +- PDFs containing JPEG 2000-encoded content will be converted to JPEG + encoding, which may introduce compression artifacts, if Ghostscript + PDF/A is enabled. +- Ghostscript may transcode grayscale and color images, either lossy to + lossless or lossless to lossy, based on an internal algorithm. This + behavior can be suppressed by setting ``--pdfa-image-compression`` to + ``jpeg`` or ``lossless`` to set all images to one type or the other. + Ghostscript has no option to maintain the input image's format. + (Ghostscript 9.25+ can copy JPEG images without transcoding them; + earlier versions will transcode.) +- Ghostscript's PDF/A conversion removes any XMP metadata that is not + one of the standard XMP metadata namespaces for PDFs. In particular, + PRISM Metdata is removed. +- Ghostscript's PDF/A conversion seems to remove or deactivate + hyperlinks and other active content. + +You can use ``--output-type pdf`` to disable PDF/A conversion and produce +a standard, non-archival PDF. Regarding OCRmyPDF itself: -* PDFs that use transparency are not currently represented in the test suite -* The Python API exported by ``import ocrmypdf`` is design to help scripts that use OCRmyPDF but is not currently capable of running OCRmyPDF jobs due to limitations in an underlying library. +- PDFs that use transparency are not currently represented in the test + suite Similar programs ----------------- +================ -To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it does not meet your needs, contributions and suggestions are welcome. If not, consider one of these similar open source programs: +To the author's knowledge, OCRmyPDF is the most feature-rich and +thoroughly tested command line OCR PDF conversion tool. If it does not +meet your needs, contributions and suggestions are welcome. If not, +consider one of these similar open source programs: -* pdf2pdfocr -* pdfsandwich -* pypdfocr -* pdfbeads +- pdf2pdfocr +- pdfsandwich +- pypdfocr +- pdfbeads Web front-ends --------------- +============== -The Docker image ``ocrmypdf-alpine`` provides a web service front-end that allows files to submitted over HTTP and the results "downloaded". This is an HTTP server intended to simplify web services deployments; it is not intended to be deployed on the public internet and no real security measures to speak of. +The Docker image ``ocrmypdf`` provides a web service front-end +that allows files to submitted over HTTP and the results "downloaded". +This is an HTTP server intended to simplify web services deployments; it +is not intended to be deployed on the public internet and no real +security measures to speak of. In addition, the following third-party integrations are available: -* `Nextcloud OCR `_ is a free software plugin for the Nextcloud private cloud software +- `Nextcloud OCR `__ is a free software + plugin for the Nextcloud private cloud software -OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online `_). Users should ensure they comply with OCRmyPDF's licenses and the licenses of all dependencies. In particular, OCRmyPDF requires Ghostscript, which is licensed under AGPLv3. +OCRmyPDF is not designed to be secure against malware-bearing PDFs (see +`Using OCRmyPDF online `__). Users should ensure they +comply with OCRmyPDF's licenses and the licenses of all dependencies. In +particular, OCRmyPDF requires Ghostscript, which is licensed under +AGPLv3. + +.. |image| image:: images/bitmap_vs_svg.svg diff --git a/docs/jbig2.rst b/docs/jbig2.rst index 3ee524ca..81789b6f 100644 --- a/docs/jbig2.rst +++ b/docs/jbig2.rst @@ -1,35 +1,55 @@ .. _jbig2: +============================ Installing the JBIG2 encoder ============================ -Most Linux distributions do not include a JBIG2 encoder since JBIG2 encoding was patented for a long time. All known JBIG2 US patents have expired as of 2017, but it is possible that unknown patents exist. +Most Linux distributions do not include a JBIG2 encoder since JBIG2 +encoding was patented for a long time. All known JBIG2 US patents have +expired as of 2017, but it is possible that unknown patents exist. -JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly create smaller PDFs. If JBIG2 encoding not available, lower quality encodings will be used. +JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly +create smaller PDFs. If JBIG2 encoding not available, lower quality +encodings will be used. -JBIG2 decoding is not patented and is performed automatically by most PDF viewers. It is widely supported has been part of the PDF specification since 2001. +JBIG2 decoding is not patented and is performed automatically by most +PDF viewers. It is widely supported has been part of the PDF +specification since 2001. -On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder from source. +On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by +default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder +from source. For all other Linux, you must build a JBIG2 encoder from source: .. code-block:: bash - git clone https://github.com/agl/jbig2enc - cd jbig2enc - ./autogen.sh - ./configure && make - [sudo] make install + git clone https://github.com/agl/jbig2enc + cd jbig2enc + ./autogen.sh + ./configure && make + [sudo] make install .. _jbig2-lossy: Lossy mode JBIG2 ----------------- +================ -OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should `review the technical concerns with JBIG2 in lossy mode `_ and decide if this feature is acceptable for their use case. +OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should +`review the technical concerns with JBIG2 in lossy +mode `__ +and decide if this feature is acceptable for their use case. -JBIG2 lossy mode does achieve higher compression ratios than any other monochrome (bitonal) compression technology; for large text documents the savings are considerable. JBIG2 lossless still gives great compression ratios and is a major improvement over the older CCITT G4 standard. As explained above, there is some risk of substitution errors. +JBIG2 lossy mode does achieve higher compression ratios than any other +monochrome (bitonal) compression technology; for large text documents +the savings are considerable. JBIG2 lossless still gives great +compression ratios and is a major improvement over the older CCITT G4 +standard. As explained above, there is some risk of substitution errors. -To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``. ``--optimize {1,2,3}`` are necessary for the argument to take effect also required. Also, a JBIG2 encoder must be installed as described in the previous section. +To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``. +``--optimize {1,2,3}`` are necessary for the argument to take effect +also required. Also, a JBIG2 encoder must be installed as described in +the previous section. -*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by default.* +*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by +default.* diff --git a/docs/languages.rst b/docs/languages.rst index 5b5a8853..45dfac6f 100644 --- a/docs/languages.rst +++ b/docs/languages.rst @@ -1,16 +1,29 @@ .. _lang-packs: +==================================== Installing additional language packs ==================================== -OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English. +OCRmyPDF uses Tesseract for OCR, and relies on its language packs for all languages. +On most platforms, English is installed with Tesseract by default, but not always. -Tesseract supports `most languages `_. +Tesseract supports `most +languages `__. +Languages are identified by standardized three-letter codes (called ISO 639-2 Alpha-3). +Tesseract's documentation also lists the three-letter code for your language. +Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others +are not, e.g. German is ``deu`` and French is ``fra``. -For Linux users, you can often find packages that provide language packs: +After you have installed a language pack, you can use it with ``ocrmypdf -l ``, +for example ``ocrmypdf -l spa``. For multilingual documents, you can specify +all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French. +English is assumed by default unless other language(s) are specified. + +For Linux users, you can often find packages that provide language +packs: Debian and Ubuntu users ------------------------ +======================= .. code-block:: bash @@ -20,11 +33,13 @@ Debian and Ubuntu users # Install Chinese Simplified language pack apt-get install tesseract-ocr-chi-sim -You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple -languages can be requested using either ``-l eng+fre`` (English and French) or ``-l eng -l fre``. +You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as +to what languages it should search for. Multiple languages can be +requested using either ``-l eng+fra`` (English and French) or +``-l eng -l fra``. Fedora users ------------- +============ .. code-block:: bash @@ -34,16 +49,28 @@ Fedora users # Install Chinese Simplified language pack dnf install tesseract-langpack-chi_sim -You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to -what languages it should search for. Multiple languages can be requested using -either ``-l eng+fre`` (English and French) or ``-l eng -l fre``. +You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as +to what languages it should search for. Multiple languages can be +requested using either ``-l eng+fra`` (English and French) or +``-l eng -l fra``. macOS users ------------ +=========== -You can install additional language packs by :ref:`installing Tesseract using Homebrew with all language packs `. +You can install additional language packs by +:ref:`installing Tesseract using Homebrew with all language packs `. Docker users ------------- +============ -Users of the OCRmyPDF Docker image should install language packs into a derived Docker image as :ref:`described in that section `. +Users of the OCRmyPDF Docker image should install language packs into a +derived Docker image as +:ref:`described in that section `. + +Windows users +============= + +The Tesseract installer provided by Chocolatey currently includes only English language. +To install other languages, download the respective language pack (``.traineddata`` file) +from https://github.com/tesseract-ocr/tessdata/ and place it in +``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed). diff --git a/docs/optimizer.rst b/docs/optimizer.rst new file mode 100644 index 00000000..02f0be21 --- /dev/null +++ b/docs/optimizer.rst @@ -0,0 +1,75 @@ +================ +PDF optimization +================ + +OCRmyPDF includes an image-oriented PDF optimizer. By default, the optimizer +runs with safe settings with the goal of improving compression at no loss of +quality. At higher optimization levels, lossy optimizations may be applied and +tuned. Optimization occurs after OCR, and only if OCR succeeded. It does not +perform other possible optimizations such as deduplicating resources, +consolidating fonts, simplifying vector drawings, or anything of that nature. + +Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables +optimization and ``3`` implements all options. ``1``, the default, performs only +safe and lossless optimizations. (This is similar to GCC's optimization +parameter.) The exact type of optimizations performed will vary over time. + +PDF optimization requires third-party, optional tools for certain optimizations. +If these are not installed or cannot be found by OCRmyPDF, optimization will not +be as good. + +Optimizations that always occurs +================================ + +OCRmyPDF will automatically replace obsolete or inferior compression schemes +such as RLE or LZW with superior schemes such as Deflate and converting +monochrome images to CCITT G4. Since this is harmless it always occurs and there +is no way to disable it. Other non-image compressed objects are compressed as +well. + +Fast web view +============= + +OCRmyPDF automatically optimizes PDFs for "fast web view" in Adobe Acrobat's +parlance, or equivalently, linearizes PDFs so that the resources they reference +are presented in the order a viewer needs them for sequential display. This +reduces the latency of viewing a PDF both online and from local storage. This +actually slightly increases the file size. + +To disable this optimization and all others, use ``ocrmypdf --optimize 0 ...`` +or the shorthand ``-O0``. + +Lossless optimizations +====================== + +At optimization level ``-O1`` (the default), OCRmyPDF will also attempt lossless +image optimization. + +If a JBIG2 encoder is available, then monochrome images will be converted to +JBIG2, with the potential for huge savings on large black and white images, +since JBIG2 is far more efficient than any other monochrome (bi-level) +compression. (All known US patents related to JBIG2 have probably expired, but +it remains the responsibility of the user to supply a JBIG2 encoder such as +`jbig2enc `__. OCRmyPDF does not implement +JBIG2 encoding on its own.) + +OCRmyPDF currently does not attempt to recompress losslessly compressed objects +more aggressively. + +Lossy optimizations +=================== + +At optimization level ``-O2`` and ``-O3``, OCRmyPDF will some attempt lossy +image optimization. + +If ``pngquant`` is installed, OCRmyPDF will use it to perform quantize paletted +images to reduce their size. + +The quality of JPEGs may be lowered, on the assumption that a lower quality +image may be suitable for storage after OCR. + +It is not possible to optimize all image types. Uncommon image types may be +skipped by the optimizer. + +OCRmyPDF provides :ref:`lossy mode JBIG2 ` as an advanced feature +that additional requires the argument ``--jbig2-lossy``. diff --git a/docs/pdfsecurity.rst b/docs/pdfsecurity.rst new file mode 100644 index 00000000..04ad4e90 --- /dev/null +++ b/docs/pdfsecurity.rst @@ -0,0 +1,161 @@ +=================== +PDF security issues +=================== + + OCRmyPDF should only be used on PDFs you trust. It is not designed to + protect you against malware. + +Recognizing that many users have an interest in handling PDFs and +applying OCR to PDFs they did not generate themselves, this article +discusses the security implications of PDFs and how users can protect +themselves. + +The disclaimer applies: this software has no warranties of any kind. + +PDFs may contain malware +======================== + +PDF is a rich, complex file format. The official PDF 1.7 specification, +ISO 32000:2008, is hundreds of pages long and references several annexes +each of which are similar in length. PDFs can contain video, audio, XML, +JavaScript and other programming, and forms. In some cases, they can +open internet connections to pre-selected URLs. All of these possible +attack vectors. + +In short, PDFs `may contain +viruses `__. + +This +`article `__ +describes a high-paranoia method which allows potentially hostile PDFs +to be viewed and rasterized safely in a disposable virtual machine. A +trusted PDF created in this manner is converted to images and loses all +information making it searchable and losing all compression. OCRmyPDF +could be used restore searchability. + +How OCRmyPDF processes PDFs +=========================== + +OCRmyPDF must open and interpret your PDF in order to insert an OCR +layer. First, it runs all PDFs through +`pikepdf `__, a library based on +`qpdf `__, a program that repairs PDFs +with syntax errors. This is done because, in the author's experience, a +significant number of PDFs in the wild especially those created by +scanners are not well-formed files. qpdf makes it more likely that +OCRmyPDF will succeed, but offers no security guarantees. qpdf is also +used to split the PDF into single page PDFs. + +Finally, OCRmyPDF rasterizes each page of the PDF using +`Ghostscript `__ in ``-dSAFER`` mode. + +Depending on the options specified, OCRmyPDF may graft the OCR layer +into the existing PDF or it may essentially reconstruct ("re-fry") a +visually identical PDF that may be quite different at the binary level. +That said, OCRmyPDF is not a tool designed for sanitizing PDFs. + +.. _ocr-service: + +Using OCRmyPDF online or as a service +===================================== + +OCRmyPDF is not designed for use as a public web service where a +malicious user could upload a chosen PDF. In particular, it is not +necessarily secure against PDF malware or PDFs that cause denial of +service. OCRmyPDF relies on Ghostscript, and therefore, if deployed +online one should be prepared to comply with Ghostscript's Affero GPL +license, and any other licenses. + +Setting aside these concerns, a side effect of OCRmyPDF is it may +incidentally sanitize PDFs that contain certain types of malware. It +repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF +structures that are part of an attack. When PDF/A output is selected +(the default), the input PDF is partially reconstructed by Ghostscript. +When ``--force-ocr`` is used, all pages are rasterized and reconverted +to PDF, which could remove malware in embedded images. + +OCRmyPDF should be relatively safe to use in a trusted intranet, with +some considerations: + +Limiting CPU usage +------------------ + +OCRmyPDF will attempt to use all available CPUs and storage, so +executing ``nice ocrmypdf`` or limiting the number of jobs with the +``-j`` argument may ensure the server remains available. Another option +would be run OCRmyPDF jobs inside a Docker container, a virtual machine, +or a cloud instance, which can impose its own limits on CPU usage and be +terminated "from orbit" if it fails to complete. + +Temporary storage requirements +------------------------------ + +OCRmyPDF will use a large amount of temporary storage for its work, +proportional to the total number of pixels needed to rasterize the PDF. +The raster image of a 8.5×11" color page at 300 DPI takes 25 MB +uncompressed; OCRmyPDF saves its intermediates as PNG, but that still +means it requires about 9 MB per intermediate based on average +compression ratios. Multiple intermediates per page are also required, +depending on the command line given. A rule of thumb would be to allow +100 MB of temporary storage per page in a file – meaning that a small +cloud servers or small VM partitions should be provisioned with plenty +of extra space, if say, a 500 page file might be sent. + +To check temporary storage usage on actual files, run +``ocrmypdf -k ...`` which will preserve and print the path to temporary +storage when the job is done. + +To change where temporary files are stored, change the ``TMPDIR`` +environment variable for ocrmypdf's environment. (Python's +``tempfile.gettempdir()`` returns the root directory in which temporary +files will be stored.) For example, one could redirect ``TMPDIR`` to a +large RAM disk to avoid wear on HDD/SSD and potentially improve +performance. On Amazon Web Services, ``TMPDIR`` can be set to `empheral +storage `__. + +Timeouts +-------- + +To prevent excessively long OCR jobs consider setting +``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big`` +is particularly helpful if your PDFs include documents such as reports +on standard page sizes with large images attached - often large images +are not worth OCR'ing anyway. + +Commercial alternatives +----------------------- + +The author also provides professional services that include OCR and +building databases around PDFs, and is happy to provide consultation. + +Abbyy Cloud OCR is a viable commercial alternative with a web services +API. + +Password protection, digital signatures and certification +========================================================= + +Password protected PDFs usually have two passwords, and owner and user +password. When the user password is set to empty, PDF readers will open +the file automatically and marked it as "(SECURED)". While not as +reliable as a digital signature, this indicates that whoever set the +password approved of the file at that time. When the user password is +set, the document cannot be viewed without the password. + +Either way, OCRmyPDF does not remove passwords from PDFs and exits with +an error on encountering them. + +``qpdf`` can remove passwords. If the owner and user password are set, a +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. + +After OCR is applied, password protection is not permitted on PDF/A +documents but the file can be converted to regular PDF. + +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 +to remove the signature image and apply it to other files. This practice +offers no real security. + +Important documents can be digitally signed and certified to attest to +their authorship. OCRmyPDF cannot do this. Open source tools such as +pdfbox (Java) have this capability as does Adobe Acrobat. diff --git a/docs/performance.rst b/docs/performance.rst new file mode 100644 index 00000000..82625fc0 --- /dev/null +++ b/docs/performance.rst @@ -0,0 +1,22 @@ +=========== +Performance +=========== + +Some users have noticed that current versions of OCRmyPDF do not run as quickly +as some older versions (specifically 6.x and older). This is because OCRmyPDF +added image optimization as a postprocessing step, and it is enabled by default. + +Speed +===== + +If running OCRmyPDF quickly is your main goal, you can use settings such as: + +* ``--optimize 0`` to disable file size optimization +* ``--output-type pdf`` to disable PDF/A generation +* ``--fast-web-view 0`` to disable fast web view optimization +* ``--skip-big`` to skip large images, if some pages have large images + +You can also avoid: + +* ``--force-ocr`` +* Image preprocessing diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 00000000..952af3fd --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,200 @@ +======= +Plugins +======= + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL + NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + RFC 2119. + +You can use plugins to customize the behavior of OCRmyPDF at certain points of +interest. + +Currently, it is possible to: + +- add new command line arguments +- override the decision for whether or not to perform OCR on a particular file +- modify the image is about to be sent for OCR +- modify the page image before it is converted to PDF +- replace the Tesseract OCR with another OCR engine that has similar behavior +- replace Ghostscript with another PDF to image converter (rasterizer) or + PDF/A generator + +OCRmyPDF plugins are based on the Python ``pluggy`` package and conform to its +conventions. Note that: plugins installed with as setuptools entrypoints are +not checked currently, because OCRmyPDF assumes you may not want to enable +plugins for all files. + +Script plugins +============== + +Script plugins may be called from the command line, by specifying the name of a file. +Script plugins may be convenient for informal or "one-off" plugins, when a certain +batch of files needs a special processing step for example. + +.. code-block:: bash + + ocrmypdf --plugin ocrmypdf_example_plugin.py input.pdf output.pdf + +Multiple plugins may be installed by issuing the ``--plugin`` argument multiple times. + +Packaged plugins +================ + +Installed plugins may be installed into the same virtual environment as OCRmyPDF +is installed into. They may be invoked using Python standard module naming. +If you are intending to distribute a plugin, please package it. + +.. code-block:: bash + + ocrmypdf --plugin ocrmypdf_fancypants.pockets.contents input.pdf output.pdf + +OCRmyPDF does not automatically import plugins, because the assumption is that +plugins affect different files differently and you may not want them activated +all the time. The command line or ``ocrmypdf.ocr(plugin='...')`` must call +for them. + +Third parties that wish to distribute packages for ocrmypdf should package them +as packaged plugins, and these modules should begin with the name ``ocrmypdf_`` +similar to ``pytest`` packages such as ``pytest-cov`` (the package) and +``pytest_cov`` (the module). + +.. note:: + + We strongly recommend plugin authors name their plugins with the prefix + ``ocrmypdf-`` (for the package name on PyPI) and ``ocrmypdf_`` (for the + module), just like pytest plugins. + +Setuptools plugins +================== + +You can also create a plugin that OCRmyPDF will always automatically load if both are +installed in the same virtual environment, using a setuptools entrypoint. + +Your package's ``setup.py`` would need to contain the following, for a plugin +named ``ocrmypdf-exampleplugin``: + +.. code-block:: python + + # sample ./setup.py file + from setuptools import setup + + setup( + name="ocrmypdf-exampleplugin", + packages=["exampleplugin"], + # the following makes a plugin available to pytest + entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]}, + ) + +Plugin requirements +=================== + +OCRmyPDF generally uses multiple worker processes. When a new worker is started, +Python will import all plugins again, including all plugins that were imported earlier. +This means that the global state of a plugin in one worker will not be shared with +other workers. As such, plugin hook implementations should be stateless, relying +only on their inputs. Hook implementations may use their input parameters to +to obtain a reference to shared state prepared by another hook implementation. +Plugins must expect that other instances of the plugin will be running +simultaneously. + +The ``context`` object that is passed to many hooks can be used to share information +about a file being worked on. Plugins must write private, plugin-specific data to +a subfolder named ``{options.work_folder}/ocrmypdf-plugin-name``. Plugins MAY +read and write files in ``options.work_folder``, but should be aware that their +semantics are subject to change. + +OCRmyPDF will delete ``options.work_folder`` when it has finished OCRing +a file, unless invoked with ``--keep-temporary-files``. + +The documentation for some plugin hooks contain a detailed description of the +execution context in which they will be called. + +Plugins should be prepared to work whether executed in worker threads or worker +processes. Generally, OCRmyPDF uses processes, but has a semi-hidden threaded +argument that simplifies debugging. + + +Plugin hooks +============ + +A plugin may provide the following hooks. Hooks must be decorated with +``ocrmypdf.hookimpl``, for example: + +.. code-block:: python + + from ocrmpydf import hookimpl + + @hookimpl + def add_options(parser): + pass + +The following is a complete list of hooks that are available, and when +they are called. + +.. _firstresult: + +**Note on firstresult hooks** + +If multiple plugins install implementations for this hook, they will be called in +the reverse of the order in which they are installed (i.e., last plugin wins). +When each hook implementation is called in order, the first implementation that +returns a value other than ``None`` will "win" and prevent execution of all other +hooks. As such, you cannot "chain" a series of plugin filters together in this +way. Instead, a single hook implementation should be responsible for any such +chaining operations. + +Custom command line arguments +----------------------------- + +.. autofunction:: ocrmypdf.pluginspec.add_options + +.. autofunction:: ocrmypdf.pluginspec.check_options + +Execution and progress reporting +-------------------------------- + +.. autoclass: ocrmypdf.pluginspec.Executor + :members: + +.. autofunction:: ocrmypdf.pluginspec.get_logging_console + +.. autofunction:: ocrmypdf.pluginspec.get_executor + +.. autofunction:: ocrmypdf.pluginspec.get_progressbar_class + +Applying special behavior before processing +------------------------------------------- + +.. autofunction:: ocrmypdf.pluginspec.validate + +PDF page to image +----------------- + +.. autofunction:: ocrmypdf.pluginspec.rasterize_pdf_page + +Modifying intermediate images +----------------------------- + +.. autofunction:: ocrmypdf.pluginspec.filter_ocr_image + +.. autofunction:: ocrmypdf.pluginspec.filter_page_image + +.. autofunction:: ocrmypdf.pluginspec.filter_pdf_page + +OCR engine +---------- + +.. autofunction:: ocrmypdf.pluginspec.get_ocr_engine + +.. autoclass:: ocrmypdf.pluginspec.OcrEngine + :members: + + .. automethod:: __str__ + +.. autoclass:: ocrmypdf.pluginspec.OrientationConfidence + +PDF/A production +---------------- + +.. autofunction:: ocrmypdf.pluginspec.generate_pdfa diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 6af0f263..2d171340 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -1,207 +1,1017 @@ +============= Release notes ============= -OCRmyPDF uses `semantic versioning `_ for its command line interface and its public API. +OCRmyPDF uses `semantic versioning `__ for its +command line interface and its public API. -The ``ocrmypdf`` package may now be imported. The public API may be useful in scripts that launch OCRmyPDF processes or that wish to use some of its features for working with PDFs. +OCRmyPDF's output messages are not considered part of the stable interface - +that is, output messages may be improved at any release level, so parsing them +may be unreliable. Use the API to depend on precise behavior. -Unfortunately, the public API does **not** expose the ability to actually OCR a PDF. This is due to a limitation in an underlying library (ruffus) that makes OCRmyPDF non-reentrant. +The public API may be useful in scripts that launch OCRmyPDF processes or that +wish to use some of its features for working with PDFs. -Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and are released publicly should probably also be licensed under GPLv3. -.. Issue regex - find: [^`]\#([0-9]{1,3})[^0-9] - replace: `#$1 `_ +v12.0.2 +======= -v8.3.0 ------- +- Fix exception thrown when using ``--remove-background`` on files containing small + images (#769). +- Improve documentation for description of adding language packs to the Docker image + and corrected name of French language pack. -- Improved the strategy for updating pages when a new image of the page was produced. We know attempt to preserve more content from the original file, for annotations in particular. +v12.0.1 +======= -- For PDFs with more than 100 pages and a sequence where one PDF page was replaced and one or more subsequent ones were skipped, an intermediate file would be corrupted while grafting OCR text, causing processing to fail. +- Fix "invalid version number" for untagged tesseract versions (#770). -- Previously, we resized the images produced by Ghostscript by a small number of pixels to ensure the output image size was an exactly what we wanted. Having discovered a way to get Ghostscript to produce the exact image sizes we require, we eliminated the resizing step. - -- Command line completions for ``bash`` are now available, in addition to ``fish``, both in ``misc/completion``. Package maintainers, please install these so users can take advantage. - -- Updated requirements. - -- pikepdf 1.3.0 is now required. - -v8.2.4 ------- - -- Fixed a false positive while checking for a certain type of PDF that only Acrobat can read. We now more accurately detect Acrobat-only PDFs. - -- OCRmyPDF holds fewer open file handles and is more prompt about releasing those it no longer needs. - -- Minor optimization: we no longer traverse the table of contents to ensure all references in it are resolved, as changes to libqpdf have made this unnecessary. - -- pikepdf 1.2.0 is now required. - -v8.2.3 ------- - -- Fixed that ``--mask-barcodes`` would occasionally leave a unwanted temporary file named ``junkpixt`` in the current working folder. - -- Fixed (hopefully) handling of Leptonica errors in an environment where a non-standard ``sys.stderr`` is present. - -- Improved help text for ``--verbose``. - -v8.2.2 ------- - -- Fixed a regression from v8.2.0, an exception that occurred while attempting to report that ``unpaper`` or another optional dependency was unavailable. - -- In some cases, ``ocrmypdf [-c|--clean]`` failed to exit with an error when ``unpaper`` is not installed. - -v8.2.1 ------- - -- This release was canceled. - -v8.2.0 ------- - -- A major improvement to our Docker image is now available thanks to hard work contributed by @mawi12345. The new Docker image, ocrmypdf-alpine, is based on Alpine Linux, and includes most of the functionality of three existed images in a smaller package. This image will replace the main Docker image eventually but for now all are being built. `See documentation for details `_. - -- Documentation reorganized especially around the use of Docker images. - -- Fixed a problem with PDF image optimization, where the optimizer would unnecessarily decompress and recompress PNG images, in some cases losing the benefits of the quantization it just had just performed. The optimizer is now capable of embedding PNG images into PDFs without transcoding them. - -- Fixed a minor regression with lossy JBIG2 image optimization. All JBIG2 candidates images were incorrectly placed into a single optimization group for the whole file, instead of grouping pages together. This usually makes a larger JBIG2Globals dictionary and results in inferior compression, so it worked less well than designed. However, quality would not be impacted. Lossless JBIG2 was entirely unaffected. - -- Updated dependencies, including pikepdf to 1.1.0. This fixes `#358 `_. - -- The install-time version checks for certain external programs have been removed from setup.py. These tests are now performed at run-time. - -- The non-standard option to override install-time checks (``setup.py install --force``) is now deprecated and prints a warning. It will be removed in a future release. - -v8.1.0 ------- - -- Added a feature, ``--unpaper-args``, which allows passing arbitrary arguments to ``unpaper`` when using ``--clean`` or ``--clean-final``. The default, very conservative unpaper settings are suppressed. - -- The argument ``--clean-final`` now implies ``--clean``. It was possible to issue ``--clean-final`` on its before this, but it would have no useful effect. - -- Fixed an exception on traversing corrupt table of contents entries (specifically, those with invalid destination objects) - -- Fixed an issue when using ``--tesseract-timeout`` and image processing features on a file with more than 100 pages. `#347 `_ - -- OCRmyPDF now always calls ``os.nice(5)`` to signal to operating systems that it is a background process. - -v8.0.1 ------- - -- Fixed an exception when parsing PDFs that are missing a required field. `#325 `_ - -- pikepdf 1.0.5 is now required, to address some other PDF parsing issues. - -v8.0.0 ------- - -No major features. The intent of this release is to sever support for older versions of certain dependencies. +v12.0.0 +======= **Breaking changes** -- Dropped support for Tesseract 3.x. Tesseract 4.0 or newer is now required. +- Due to recent security issues in pikepdf, Pillow and reportlab, we now require + newer versions of these libraries and some of their dependencies. (If necessary, + package maintainers may override these versions at their discretion; lower + versions will often work.) +- We now use the "LeaveColorUnchanged" color conversion strategy when directing + Ghostscript to create a PDF/A. Generally this is faster than performing a + color conversion, which is not always necessary. +- OCR text is now packaged in a Form XObject. This makes it easier to isolate + OCR from other document content. However, some poorly implemented PDF text + extraction algorithms may fail to detect the text. +- Many API functions have stricter parameter checking or expect keyword arguments + were they previously did not. +- Some deprecated functions in ``ocrmypdf.optimize`` were removed. +- The ``ocrmypdf.leptonica`` module is now deprecated, due to difficulties with + the current strategy of ABI binding on newer platforms like Apple Silicon. + It will be removed and replaced, either by repackaging Leptonica as an + independent library using or using a different image processing library. +- Continuous integration moved to GitHub Actions. +- We no longer depend on ``pytest_helpers_namespace`` for testing. -- Dropped support for Python 3.5. +**New features** -- Some ``ocrmypdf.pdfa`` APIs that were deprecated in v7.x were removed. This functionality has been moved to pikepdf. +- New plugin hook: ``get_progressbar_class``, for progress reporting, + allowing developers to replace the standard console progress bar with some + other mechanism, such as updating a GUI progress bar. +- New plugin hook: ``get_executor``, for replacing the concurrency model. + This is primarily to support execution on AWS Lambda, which does not support + standard Python ``multiprocessing`` due to its lack of shared memory. +- New plugin hook: ``get_logging_console``, for replacing the standard + way OCRmyPDF outputs its messages. +- New plugin hook: ``filter_pdf_page``, for modifying individual PDF + pages produced by OCRmyPDF. +- OCRmyPDF now runs on nonstandard execution environments that do not have + interprocess semaphores, such as AWS Lambda and Android Termux. If the environment + does not have semaphores, OCRmyPDF will automatically select an alternate + process executor that does not use semaphores. +- Continuous integration moved to GitHub Actions. +- We now generate an ARM64-compatible Docker image alongside the x64 image. + Thanks to @andkrause for doing most of the work in a pull request several months + ago, which we were finally able to integrate now. Also thanks to @0x326 for + review comments. + +**Fixes** + +- Fixed a possible deadlock on attempting to flush ``sys.stderr`` when older + versions of Leptonica are in use. +- Some worker processes inherited resources from their parents such as log + handlers that may have also lead to deadlocks. These resources are now released. +- Improvements to test coverage. +- Removed vestiges of support for Tesseract versions older than 4.0.0-beta1 ( + which ships with Ubuntu 18.04). +- OCRmyPDF can now parse all of Tesseract version numbers, since several + schemes have been in use. +- Fixed an issue with parsing PDFs that contain images drawn at a scale of 0. (#761) +- Removed a frequently repeated message about disabling mmap. + +v11.7.3 +======= + +- Exclude CCITT Group 3 images from being optimized. Some libraries + OCRmyPDF uses do not seem to handle this obscure compression format properly. + You may get errors or possible corrupted output images without this fix. + +v11.7.2 +======= + +- Updated pinned versions in main.txt, primarily to upgrade Pillow to 8.1.2, due + to recently disclosed security vulnerabilities in that software. +- The ``--sidecar`` parameter now causes an exception if set to the same file as + the input or output PDF. + +v11.7.1 +======= + +- Some exceptions while attempting image optimization were only logged at the debug + level, causing them to be suppressed. These errors are now logged appropriately. +- Improved the error message related to ``--unpaper-args``. +- Updated documentation to mention the new conda distribution. + +v11.7.0 +======= + +- We now support using ``--sidecar`` in conjunction with ``--pages``; these arguments + used to be mutually exclusive. (#735) +- Fixed a possible issue with PDF/A-1b generation. Acrobat complained that our PDFs use + object streams. More robust PDF/A validators like veraPDF don't consider this a + problem, but we'll honor Acrobat's objection from here on. This may increase file + size of PDF/A-1b files. PDF/A-2b files will not be affected. + +v11.6.2 +======= + +- Fixed a regression where the wrong page orientation would be produced when using + arguments such as ``--deskew --rotate-pages`` (#730). + +v11.6.1 +======= + +- Fixed an issue with attempting optimize unusually narrow-width images by excluding + these images from optimization (#732). +- Remove an obsolete compatibility shim for a version of pikepdf that is no longer + supported. + +v11.6.0 +======= + +- OCRmyPDF will now automatically register plugins from the same virtual environment + with an appropriate setuptools entrypoint. +- Refactor the plugin manager to remove unnecessary complications and make plugin + registration more automatic. +- ``PageContext`` and ``PdfContext`` are now formally part of the API, as they + should have been, since they were part of ``ocrmypdf.pluginspec``. + +v11.5.0 +======= + +- Fixed an issue where the output page size might differ by a fractional amount + due to rounding, when ``--force-ocr`` was used and the page contained objects + with multiple resolutions. +- When determining the resolution at which to rasterize a page, we now consider + printed text on the page as requiring a higher resolution. This fixes issues + with certain pages being rendered with unacceptably low resolution text, but + may increase output file sizes in some workflows where low resolution text + is acceptable. +- Added a workaround to fix an exception that occurs when trying to + ``import ocrmypdf.leptonica`` on Apple ARM silicon (or potentially, other + platforms that do not permit write+executable memory). + +v11.4.5 +======= + +- Fixed an issue where files may not be closed when the API is used. +- Improved ``setup.cfg`` with better settings for test coverage. + +v11.4.4 +======= + +- Fixed ``AttributeError: 'NoneType' object has no attribute 'userunit'``, issue #700, + related to OCRmyPDF not properly forwarded an error message from pdfminer.six. +- Adjusted typing of some arguments. +- ``ocrmypdf.ocr`` now takes a ``threading.Lock`` for reasons outlined in the + documentation. + +v11.4.3 +======= + +- Removed a redundant debug message. +- Test suite now asserts that most patched functions are called when they should be. +- Test suite now skips a test that fails on two particular versions of piekpdf. + +v11.4.2 +======= + +- Fixed support for Cygwin, hopefully. +- watcher.py: Fixed an issue with the OCR_LOGLEVEL not being interpreted. + +v11.4.1 +======= + +- Fixed an issue where invalid pages ranges passed using the ``pages`` argument, + such as "1-0" would cause unhandled exceptions. +- Accepted a user-contributed to the Synology demo script in misc/synology.py. +- Clarified documentation about change of temporary file location ``ocrmypdf.io``. +- Fixed Python wheel tag which was incorrectly set to py35 even though we long + since dropped support for Python 3.5. + +v11.4.0 +======= + +- When looking for Tesseract and Ghostscript, we now check the Windows Registry to + see if their installers registered the location of their executables. This should + help Windows users who have installed these programs to non-standard + locations. +- We now report on the progress of PDF/A conversion, since this operation is + sometimes slow. +- Improved command line completions. +- The prefix of the temporary folder OCRmyPDF creates has been changed from + ``com.github.ocrmypdf`` to ``ocrmypdf.io``. Scripts that chose to depend on this + prefix may need to be adjusted. (This has always been an implementation detail so is + not considered part of the semantic versioning "contract".) +- Fixed issue #692, where a particular file with malformed fonts would flood an + internal message cue by generating so many debug messages. +- Fixed an exception on processing hOCR files with no page record. Tesseract + is not known to generate such files. + +v11.3.4 +======= + +- Fixed an error message 'called readLinearizationData for file that is not + linearized' that may occur when pikepdf 2.1.0 is used. (Upgrading to pikepdf + 2.1.1 also fixes the issue.) +- File watcher now automatically includes ``.PDF`` in addition to ``.pdf`` to + better support case sensitive file systems. +- Some documentation and comment improvements. + +v11.3.3 +======= + +- If unpaper outputs non-UTF-8 data, quietly fix this rather than choke on the + conversion. (Possibly addresses #671.) + +v11.3.2 +======= + +- Explicitly require pikepdf 2.0.0 or newer when running on Python 3.9. (There are + concerns about the stability of pybind11 2.5.x with Python 3.9, which is used in + pikepdf 1.x.) +- Fixed another issue related to page rotation. +- Fixed an issue where image marked as image masks were not properly considered + as optimization candidates. +- On some systems, unpaper seems to be unable to process the PNGs we offer it + as input. We now convert the input to PNM format, which unpaper always accepts. + Fixes #665 and #667. +- DPI sent to unpaper is now rounded to a more reasonable number of decimal digits. +- Debug and error messages from unpaper were being suppressed. +- Some documentation tweaks. + +v11.3.1 +======= + +- Declare support for new versions: pdfminer.six 20201018 and pikepdf 2.x +- Fix warning related to ``--pdfa-image-compression`` that appears at the wrong + time. + +v11.3.0 +======= + +- The "OCR" step is describing as "Image processing" in the output messages when + OCR is disabled, to better explain the application's behavior. +- Debug logs are now only created when run as a command line, and not when OCR + is performed for an API call. It is the calling application's responsibility + to set up logging. +- For PDFs with a low number of pages, we gathered information about the input PDF + in a thread rather than process (when there are more pages). When run as a + thread, we did not close the file handle to the working PDF, leaking one file + handle per call of ``ocrmypdf.ocr``. +- Fixed an issue where debug messages send by child worker processes did not match + the log settings of parent process, causing messages to be dropped. This affected + macOS and Windows only where the parent process is not forked. +- Fixed the hookspec of rasterize_pdf_page to remove default parameters that + were not handled in an expected way by pluggy. +- Fixed another issue with automatic page rotation (#658) due to the issue above. + +v11.2.1 +======= + +- Fixed an issue where optimization of a 1-bit image with a color palette or + associated ICC that was optimized to JBIG2 could have its colors inverted. + +v11.2.0 +======= + +- Fixed an issue with optimizing PNG-type images that had soft masks or image masks. + This is a regression introduced in (or about) v11.1.0. +- Improved type checking of the ``plugins`` parameter for the ``ocrmypdf.ocr`` + API call. + +v11.1.2 +======= + +- Fixed hOCR renderer writing the text in roughly reverse order. This should not + affect reasonably smart PDF readers that properly locate the position of all + text, but may confuse those that rely on the order of objects in the content + stream. (#642) + +v11.1.1 +======= + +- We now avoid using named temporary files when using pngquant allowing containerized + pngquant installs to be used. +- Clarified an error message. +- Highest number of 1's in a release ever! + +v11.1.0 +======= + +- Fixed page rotation issues: #634, #589. +- Fixed some cases where optimization created an invalid image such as a + 1-bit "RGB" image: #629, #620. +- Page numbers are now displayed in debug logs when pages are being grafted. +- ocrmypdf.optimize.rewrite_png and ocrmypdf.optimize.rewrite_png_as_g4 were + marked deprecated. Strictly speaking these should have been internal APIs, + but they were never hidden. +- As a precaution, pikepdf mmap-based file access has been disabled due to a + rare race condition that causes a crash when certain objects are deallocated. + The problem is likely in pikepdf's dependency pybind11. +- Extended the example plugin to demonstrate conversion to mono. + +v11.0.2 +======= + +- Fixed issue #612, TypeError exception. Fixed by eliminating unnecessary repair of + input PDF metadata in memory. + +v11.0.1 +======= + +- Blacklist pdfminer.six 20200720, which has a regression fixed in 20200726. +- Approve img2pdf 0.4 as it passes tests. +- Clarify that the GPL-3 portion of pdfa.py was removed with the changes in v11.0.0; + the debian/copyright file did not properly annotate this change. + +v11.0.0 +======= + +- Project license changed to Mozilla Public License 2.0. Some miscellaneous + code is now under MIT license and non-code content/media remains under + CC-BY-SA 4.0. License changed with approval of all people who were found + to have contributed to GPLv3 licensed sections of the project. (#600) +- Because the license changed, this is being treated as a major version number + change; however, there are no known breaking changes in functional behavior + or API compared to v10.x. + +v10.3.3 +======= + +- Fixed a "KeyError: 'dpi'" error message when using ``--threshold`` on an image. + (#607) + +v10.3.2 +======= + +- Fixed a case where we reported "no reason" for a file size increase, when we + could determine the reason. +- Enabled support for pdfminer.six 20200726. + +v10.3.1 +======= + +- Fixed a number of test suite failures with pdfminer.six older than veresion 20200402. +- Enabled support for pdfminer.six 20200720. + +v10.3.0 +======= + +- Fixed an issue where we would consider images that were already JBIG2-encoded + for optimization, potentially producing a less optimized image than the original. + We do not believe this issue would ever cause an image to loss fidelity. +- Where available, pikepdf memory mapping is now used. This improves performance. +- When Leptonica 1.79+ is installed, use its new error handling API to avoid + a "messy" redirection of stderr which was necessary to capture its error + messages. +- For older versions of Leptonica, added a new thread level lock. This fixes a + possible race condition in handling error conditions in Leptonica (although + there is no evidence it ever caused issues in practice). +- Documentation improvements and more type hinting. + +v10.2.1 +======= + +- Disabled calculation of text box order with pdfminer. We never needed this result + and it is expensive to calculate on files with complex pre-existing text. +- Fixed plugin manager to accept ``Path(plugin)`` as a path to a plugin. +- Fixed some typing errors. +- Documentation improvements. + +v10.2.0 +======= + +- Update Docker image to use Ubuntu 20.04. +- Fixed issue PDF/A acquires title "Untitled" after conversion. (#582) +- Fixed a problem where, when using ``--pdf-renderer hocr``, some text would + be missing from the output when using a more recent version of Tesseract. + Tesseract began adding more detailed markup about the semantics of text + that our HOCR transform did not recognize, so it ignored them. This option is + not the default. If necessary ``--redo-ocr`` also redoing OCR to fix such issues. +- Fixed an error in Python 3.9 beta, due to removal of deprecated + ``Element.getchildren()``. (#584) +- Implemented support using the API with ``BytesIO`` and other file stream objects. + (#545) + +v10.1.1 +======= + +- Fixed ``OMP_THREAD_LIMIT`` set to invalid value error messages on some input + files. (The error was harmless, apart from less than optimal performance in + some cases.) + +v10.1.0 +======= + +- Previously, we ``--clean-final`` would cause an unpaper-cleaned page image to + be produced twice, which was necessary in some cases but not in general. We + now take this optimization opportunity and reuse the image if possible. +- We now provide PNG files as input to unpaper, since it accepts them, instead + of generating PPM files which can be very large. This can improve performance + and temporary disk usage. +- Documentation updated for plugins. + +v10.0.1 +======= + +- Fixed regression when ``-l lang1+lang2`` is used from command line. + +v10.0.0 +======= + +**Breaking changes** + +- Support for pdfminer.six version 20181108 has been dropped, along with a + monkeypatch that made this version work. +- Output messages are now displayed in color (when supported by the terminal) + and prefixes describing the severity of the message are removed. As such + programs that parse OCRmyPDF's log message will need to be revised. (Please + consider using OCRmyPDF as a library instead.) +- The minimum version for certain dependencies has increased. +- Many API changes; see developer changes. +- The Python libraries pluggy and coloredlogs are now required. + +**New features and improvements** + +- PDF page scanning is now parallelized across CPUs, speeding up this phase + dramatically for files with a high page counts. +- PDF page scanning is optimized, addressing some performance regressions. +- PDF page scanning is no longer run on pages that are not selected when the + ``--pages`` argument is used. +- PDF page scanning is now independent of Ghostscript, ending our past reliance + on this occasionally unstable feature in Ghostscript. +- A plugin architecture has been added, currently allowing one to more easily + use a different OCR engine or PDF renderer from Tesseract and Ghostscript, + respectively. A plugin can also override some decisions, such changing + the OCR settings after initial scanning. +- Colored log messages. + +**Developer changes** + +- The test spoofing mechanism, used to test correct handling of failures in + Tesseract and Ghostscript, has been removed in favor of using plugins for + testing. The spoofing mechanism was fairly complex and required many special + hacks for Windows. +- Code describing the resolution in DPI of images was refactored into a + ``ocrmypdf.helpers.Resolution`` class. +- The module ``ocrmypdf._exec`` is now private to OCRmyPDF. +- The ``ocrmypdf.hocrtransform`` module has been updated to follow PEP8 naming + conventions. +- Ghostscript is no longer used for finding the location of text in PDFs, and + APIs related to this feature have been removed. +- Lots of internal reorganization to support plugins. + +v9.8.2 +====== + +- Fixed an issue where OCRmyPDF would ignore text inside Form XObject when + making certain decisions about whether a document already had text. +- Fixed file size increase warning to take overhead of small files into account. +- Added instructions for installing on Cygwin. + +v9.8.1 +====== + +- Fixed an issue where unexpected files in the ``%PROGRAMFILES%\gs`` directory + (Windows) caused an exception. +- Mark pdfminer.six 20200517 as supported. +- If jbig2enc is missing and optimization is requested, a warning is issued + instead of an error, which was the intended behavior. +- Documentation updates. + +v9.8.0 +====== + +- Fixed issue where only the first PNG (FlateDecode) image in a file would be + considered for optimization. File sizes should be improved from here on. +- Fixed a startup crash when the chosen language was Japanese (#543). +- Added options to configure polling and log level to watcher.py. + +v9.7.2 +====== + +- Fixed an issue with ``ocrmypdf.ocr(...language=)`` not accepting a list of + languages as documented. +- Updated setup.py to confirm that pdfminer.six version 20200402 is supported. + +v9.7.1 +====== + +- Fixed version check failing when used with qpdf 10.0.0. +- Added some missing type annotations. +- Updated documentation to warn about need for "ifmain" guard and Windows. + +v9.7.0 +====== + +- Fixed an error in watcher.py if ``OCR_JSON_SETTINGS`` was not defined. +- Ghostscript 9.51 is now blacklisted, due to numerous problems with this version. +- Added a workaround for a problem with "txtwrite" in Ghostscript 9.52. +- Fixed an issue where the incorrect number of threads used was shown when + ``OMP_THREAD_LIMIT`` was manipulated. +- Removed a possible performance bottlenecks for files that use hundreds to + thousands of images on the same page. +- Documentation improvements. +- Optimization will now be applied to some monochrome images that have a color + profile defined instead of only black and white. +- ICC profiles are consulted when determining the simplified colorspace of an + image. + +v9.6.1 +====== + +- Documentation improvements - thanks to many users for their contributions! + + - Fixed installation instructions for ArchLinux (@pigmonkey) + - Updated installation instructions for FreeBSD and other OSes (@knobix) + - Added instructions for using Docker Compose with watchdog (@ianalexander, + @deisi) + - Other miscellany (@mb720, @toy, @caiofacchinato) + - Some scripts provided in the documentation have been migrated out so that + they can be copied out as whole files, and to ensure syntax checking + is maintained. + +- Fixed an error that caused bash completions to fail on macOS. (#502, #504; + @AlexanderWillner) +- Fixed a rare case where OCRmyPDF threw an exception while processing a PDF + with the wrong object type in its ``/Trailer /Info``. The error is now logged + and incorrect object is ignored. (#497) +- Removed potentially non-free file ``enron1.pdf`` and simplified the test that + used it. +- Removed potentially non-free file ``misc/media/logo.afdesign``. + +v9.6.0 +====== + +- Fixed a regression with transferring metadata from the input PDF to the output + PDF in certain situations. +- pdfminer.six is now supported up to version 2020-01-24. +- Messages are explaining page rotation decisions are now shown at the standard + verbosity level again when ``--rotate-pages``. In some previous version they + were set to debug level messages that only appeared with the parameter ``-v1``. +- Improvements to ``misc/watcher.py``. Thanks to @ianalexander and @svenihoney. +- Documentation improvements. + +v9.5.0 +====== + +- Added API functions to measure OCR quality. +- Modest improvements to handling PDFs with difficult/non compliant metadata. + +v9.4.0 +====== + +- Updated recommended dependency versions. +- Improvements to test coverage and changes to facilitate better measurement of + test coverage, such as when tests run in subprocesses. +- Improvements to error messages when Leptonica is not installed correctly. +- Fixed use of pytest "session scope" that may have caused some intermittent + CI failures. +- When the argument ``--keep-temporary-files`` or verbosity is set to ``-v1``, + a debug log file is generated in the working temporary folder. + +v9.3.0 +====== + +- Improved native Windows support: we now check in the obvious places in + the "Program Files" folders installations of Tesseract and Ghostscript, + rather than relying on the user to edit ``PATH`` to specify their location. + The ``PATH`` environment variable can still be used to differentiate when + multiple installations are present or the programs are installed to non- + standard locations. +- Fixed an exception on parsing Ghostscript error messages. +- Added an improved example demonstrating how to set up a watched folder + for automated OCR processing (thanks to @ianalexander for the contribution). + +v9.2.0 +====== + +- Native Windows is now supported. +- Continuous integration moved to Azure Pipelines. +- Improved test coverage and speed of tests. +- Fixed an issue where a page that was originally a JPEG would be saved as a + PNG, increasing file size. This occurred only when a preprocessing option + was selected along with ``--output-type=pdf`` and all images on the original + page were JPEGs. Regression since v7.0.0. +- OCRmyPDF no longer depends on the QPDF executable ``qpdf`` or ``libqpdf``. + It uses pikepdf (which in turn depends on ``libqpdf``). Package maintainers + should adjust dependencies so that OCRmyPDF no longer calls for libqpdf on + its own. For users of Python binary wheels, this change means a separate + installation of QPDF is no longer necessary. This change is mainly to + simplify installation on Windows. +- Fixed a rare case where log messages from Tesseract would be discarded. +- Fixed incorrect function signature for pixFindPageForeground, causing + exceptions on certain platforms/Leptonica versions. + +v9.1.1 +====== + +- Expand the range of pdfminer.six versions that are supported. +- Fixed Docker build when using pikepdf 1.7.0. +- Fixed documentation to recommend using pip from get-pip.py. + +v9.1.0 +====== + +- Improved diagnostics when file size increases at output. Now warns if JBIG2 + or pngquant were not available. +- pikepdf 1.7.0 is now required, to pick up changes that remove the need for + a source install on Linux systems running Python 3.8. + +v9.0.5 +====== + +- The Alpine Docker image (jbarlow83/ocrmypdf-alpine) has been dropped due to + the difficulties of supporting Alpine Linux. +- The primary Docker image (jbarlow83/ocrmypdf) has been improved to take on + the extra features that used to be exclusive to the Alpine image. +- No changes to application code. +- pdfminer.six version 20191020 is now supported. + +v9.0.4 +====== + +- Fixed compatibility with Python 3.8 (but requires source install for the moment). +- Fixed Tesseract settings for ``--user-words`` and ``--user-patterns``. +- Changed to pikepdf 1.6.5 (for Python 3.8). +- Changed to Pillow 6.2.0 (to mitigate a security vulnerability in earlier Pillow). +- A debug message now mentions when English is automatically selected if the locale + is not English. + +v9.0.3 +====== + +- Embed an encoded version of the sRGB ICC profile in the intermediate + Postscript file (used for PDF/A conversion). Previously we included the + filename, which required Postscript to run with file access enabled. For + security, Ghostscript 9.28 enables ``-dSAFER`` and as such, no longer + permits access to any file by default. This fix is necessary for + compatibility with Ghostscript 9.28. +- Exclude a test that sometimes times out and fails in continuous integration + from the standard test suite. + +v9.0.2 +====== + +- The image optimizer now skips optimizing flate (PNG) encoded images in some + situations where the optimization effort was likely wasted. +- The image optimizer now ignores images that specify arbitrary decode arrays, + since these are rare. +- Fixed an issue that caused inversion of black and white in monochrome images. + We are not certain but the problem seems to be linked to Leptonica 1.76.0 and + older. +- Fixed some cases where the test suite failed if + English or German Tesseract language packs were not installed. +- Fixed a runtime error if the Tesseract English language is not installed. +- Improved explicit closing of Pillow images after use. +- Actually fixed of Alpine Docker image build. +- Changed to pikepdf 1.6.3. + +v9.0.1 +====== + +- Fixed test suite failing when either of optional dependencies unpaper and + pngquant were missing. +- Attempted fix of Alpine Docker image build. +- Documented that FreeBSD ports are now available. +- Changed to pikepdf 1.6.1. + +v9.0.0 +====== + +**Breaking changes** + +- The ``--mask-barcodes`` experimental feature has been dropped due to poor + reliability and occasional crashes, both due to the underlying library that + implements this feature (Leptonica). +- The ``-v`` (verbosity level) parameter now accepts only ``0``, ``1``, and + ``2``. +- Dropped support for Tesseract 4.00.00-alpha releases. Tesseract 4.0 beta and + later remain supported. +- Dropped the ``ocrmypdf-polyglot`` and ``ocrmypdf-webservice`` images. + +**New features** + +- Added a high level API for applications that want to integrate OCRmyPDF. + Special thanks to Martin Wind (@mawi1988) whose made significant contributions + to this effort. +- Added progress bars for long-running steps. ■■■■■■■□□ +- We now create linearized ("fast web view") PDFs by default. The new parameter + ``--fast-web-view`` provides control over when this feature is applied. +- Added a new ``--pages`` feature to limit OCR to only a specific page range. + The list may contain commas or single pages, such as ``1, 3, 5-11``. +- When the number of pages is small compared to the number of allowed jobs, we + run Tesseract in multithreaded (OpenMP) mode when available. This should + improve performance on files with low page counts. +- Removed dependency on ``ruffus``, and with that, the non-reentrancy + restrictions that previous made an API impossible. +- Output and logging messages overhauled so that ocrmypdf may be integrated + into applications that use the logging module. +- pikepdf 1.6.0 is required. +- Added a logo. 😊 + +**Bug fixes** + +- Pages with vector artwork are treated as full color. Previously, vectors + were ignored when considering the colorspace needed to cover a page, which + could cause loss of color under certain settings. +- Test suite now spawns processes less frequently, allowing more accurate + measurement of code coverage. +- Improved test coverage. +- Fixed a rare division by zero (if optimization produced an invalid file). +- Updated Docker images to use newer versions. +- Fixed images encoded as JBIG2 with a colorspace other than ``/DeviceGray`` + were not interpreted correctly. +- Fixed a OCR text-image registration (i.e. alignment) problem when the page + when MediaBox had a nonzero corner. + +v8.3.2 +====== + +- Dropped workaround for macOS that allowed it work without pdfminer.six, + now a proper sdist release of pdfminer.six is available. + +- pikepdf 1.5.0 is now required. + +v8.3.1 +====== + +- Fixed an issue where PDFs with malformed metadata would be rendered as + blank pages. `#398 `_. + +v8.3.0 +====== + +- Improved the strategy for updating pages when a new image of the page + was produced. We now attempt to preserve more content from the + original file, for annotations in particular. +- For PDFs with more than 100 pages and a sequence where one PDF page + was replaced and one or more subsequent ones were skipped, an + intermediate file would be corrupted while grafting OCR text, causing + processing to fail. This is a regression, likely introduced in + v8.2.4. +- Previously, we resized the images produced by Ghostscript by a small + number of pixels to ensure the output image size was an exactly what + we wanted. Having discovered a way to get Ghostscript to produce the + exact image sizes we require, we eliminated the resizing step. +- Command line completions for ``bash`` are now available, in addition + to ``fish``, both in ``misc/completion``. Package maintainers, please + install these so users can take advantage. +- Updated requirements. +- pikepdf 1.3.0 is now required. + +v8.2.4 +====== + +- Fixed a false positive while checking for a certain type of PDF that + only Acrobat can read. We now more accurately detect Acrobat-only + PDFs. +- OCRmyPDF holds fewer open file handles and is more prompt about + releasing those it no longer needs. +- Minor optimization: we no longer traverse the table of contents to + ensure all references in it are resolved, as changes to libqpdf have + made this unnecessary. +- pikepdf 1.2.0 is now required. + +v8.2.3 +====== + +- Fixed that ``--mask-barcodes`` would occasionally leave a unwanted + temporary file named ``junkpixt`` in the current working folder. +- Fixed (hopefully) handling of Leptonica errors in an environment + where a non-standard ``sys.stderr`` is present. +- Improved help text for ``--verbose``. + +v8.2.2 +====== + +- Fixed a regression from v8.2.0, an exception that occurred while + attempting to report that ``unpaper`` or another optional dependency + was unavailable. +- In some cases, ``ocrmypdf [-c|--clean]`` failed to exit with an error + when ``unpaper`` is not installed. + +v8.2.1 +====== + +- This release was canceled. + +v8.2.0 +====== + +- A major improvement to our Docker image is now available thanks to + hard work contributed by @mawi12345. The new Docker image, + ocrmypdf-alpine, is based on Alpine Linux, and includes most of the + functionality of three existed images in a smaller package. This + image will replace the main Docker image eventually but for now all + are being built. `See documentation for + details `__. +- Documentation reorganized especially around the use of Docker images. +- Fixed a problem with PDF image optimization, where the optimizer + would unnecessarily decompress and recompress PNG images, in some + cases losing the benefits of the quantization it just had just + performed. The optimizer is now capable of embedding PNG images into + PDFs without transcoding them. +- Fixed a minor regression with lossy JBIG2 image optimization. All + JBIG2 candidates images were incorrectly placed into a single + optimization group for the whole file, instead of grouping pages + together. This usually makes a larger JBIG2Globals dictionary and + results in inferior compression, so it worked less well than + designed. However, quality would not be impacted. Lossless JBIG2 was + entirely unaffected. +- Updated dependencies, including pikepdf to 1.1.0. This fixes + `#358 `__. +- The install-time version checks for certain external programs have + been removed from setup.py. These tests are now performed at + run-time. +- The non-standard option to override install-time checks + (``setup.py install --force``) is now deprecated and prints a + warning. It will be removed in a future release. + +v8.1.0 +====== + +- Added a feature, ``--unpaper-args``, which allows passing arbitrary + arguments to ``unpaper`` when using ``--clean`` or ``--clean-final``. + The default, very conservative unpaper settings are suppressed. +- The argument ``--clean-final`` now implies ``--clean``. It was + possible to issue ``--clean-final`` on its before this, but it would + have no useful effect. +- Fixed an exception on traversing corrupt table of contents entries + (specifically, those with invalid destination objects) +- Fixed an issue when using ``--tesseract-timeout`` and image + processing features on a file with more than 100 pages. + `#347 `__ +- OCRmyPDF now always calls ``os.nice(5)`` to signal to operating + systems that it is a background process. + +v8.0.1 +====== + +- Fixed an exception when parsing PDFs that are missing a required + field. `#325 `__ +- pikepdf 1.0.5 is now required, to address some other PDF parsing + issues. + +v8.0.0 +====== + +No major features. The intent of this release is to sever support for +older versions of certain dependencies. + +**Breaking changes** + +- Dropped support for Tesseract 3.x. Tesseract 4.0 or newer is now + required. +- Dropped support for Python 3.5. +- Some ``ocrmypdf.pdfa`` APIs that were deprecated in v7.x were + removed. This functionality has been moved to pikepdf. **Other changes** -- Fixed an unhandled exception when attempting to mask barcodes. `#322 `_ - -- It is now possible to use ocrmypdf without pdfminer.six, to support distributions that do not have it or cannot currently use it (e.g. Homebrew). Downstream maintainers should include pdfminer.six if possible. - -- A warning is now issue when PDF/A conversion removes some XMP metadata from the input PDF. (Only a "whitelist" of certain XMP metadata types are allowed in PDF/A.) - -- Fixed several issues that caused PDF/As to be produced with nonconforming XMP metadata (would fail validation with veraPDF). - -- Fixed some instances where invalid DocumentInfo from a PDF cause XMP metadata creation to fail. - -- Fixed a few documentation problems. - -- pikepdf 1.0.2 is now required. +- Fixed an unhandled exception when attempting to mask barcodes. + `#322 `__ +- It is now possible to use ocrmypdf without pdfminer.six, to support + distributions that do not have it or cannot currently use it (e.g. + Homebrew). Downstream maintainers should include pdfminer.six if + possible. +- A warning is now issue when PDF/A conversion removes some XMP + metadata from the input PDF. (Only a "whitelist" of certain XMP + metadata types are allowed in PDF/A.) +- Fixed several issues that caused PDF/As to be produced with + nonconforming XMP metadata (would fail validation with veraPDF). +- Fixed some instances where invalid DocumentInfo from a PDF cause XMP + metadata creation to fail. +- Fixed a few documentation problems. +- pikepdf 1.0.2 is now required. v7.4.0 ------- +====== -- ``--force-ocr`` may now be used with the new ``--threshold`` and ``--mask-barcodes`` features - -- pikepdf >= 0.9.1 is now required. - -- Changed metadata handling to pikepdf 0.9.1. As a result, metadata handling of non-ASCII characters in Ghostscript 9.25 or later is fixed. - -- chardet >= 3.0.4 is temporarily listed as required. pdfminer.six depends on it, but the most recent release does not specify this requirement. (`#326 `_) - -- python-xmp-toolkit and libexempi are no longer required. - -- A new Docker image is now being provided for users who wish to access OCRmyPDF over a simple HTTP interface, instead of the command line. - -- Increase tolerance of PDFs that overflow or underflow the PDF graphics stack. (`#325 `_) +- ``--force-ocr`` may now be used with the new ``--threshold`` and + ``--mask-barcodes`` features +- pikepdf >= 0.9.1 is now required. +- Changed metadata handling to pikepdf 0.9.1. As a result, metadata + handling of non-ASCII characters in Ghostscript 9.25 or later is + fixed. +- chardet >= 3.0.4 is temporarily listed as required. pdfminer.six + depends on it, but the most recent release does not specify this + requirement. + (`#326 `__) +- python-xmp-toolkit and libexempi are no longer required. +- A new Docker image is now being provided for users who wish to access + OCRmyPDF over a simple HTTP interface, instead of the command line. +- Increase tolerance of PDFs that overflow or underflow the PDF + graphics stack. + (`#325 `__) v7.3.1 ------- - -- Fixed performance regression from v7.3.0; fast page analysis was not selected when it should be. - -- Fixed a few exceptions related to the new ``--mask-barcodes`` feature and improved argument checking - -- Added missing detection of TrueType fonts that lack a Unicode mapping +====== +- Fixed performance regression from v7.3.0; fast page analysis was not + selected when it should be. +- Fixed a few exceptions related to the new ``--mask-barcodes`` feature + and improved argument checking +- Added missing detection of TrueType fonts that lack a Unicode mapping v7.3.0 ------- +====== -- Added a new feature ``--redo-ocr`` to detect existing OCR in a file, remove it, and redo the OCR. This may be particularly helpful for anyone who wants to take advantage of OCR quality improvements in Tesseract 4.0. Note that OCR added by OCRmyPDF before version 3.0 cannot be detected since it was not properly marked as invisible text in the earliest versions. OCR that constructs a font from visible text, such as Adobe Acrobat's ClearScan. +- Added a new feature ``--redo-ocr`` to detect existing OCR in a file, + remove it, and redo the OCR. This may be particularly helpful for + anyone who wants to take advantage of OCR quality improvements in + Tesseract 4.0. Note that OCR added by OCRmyPDF before version 3.0 + cannot be detected since it was not properly marked as invisible text + in the earliest versions. OCR that constructs a font from visible + text, such as Adobe Acrobat's ClearScan. +- OCRmyPDF's content detection is generally more sophisticated. It + learns more about the contents of each PDF and makes better + recommendations: -- OCRmyPDF's content detection is generally more sophisticated. It learns more about the contents of each PDF and makes better recommendations: + - OCRmyPDF can now detect when a PDF contains text that cannot be + mapped to Unicode (meaning it is readable to human eyes but + copy-pastes as gibberish). In these cases it recommends + ``--force-ocr`` to make the text searchable. + - PDFs containing vector objects are now rendered at more + appropriate resolution for OCR. + - We now exit with an error for PDFs that contain Adobe LiveCycle + Designer's dynamic XFA forms. Currently the open source community + does not have tools to work with these files. + - OCRmyPDF now warns when a PDF that contains Adobe AcroForms, since + such files probably do not need OCR. It can work with these files. - - OCRmyPDF can now detect when a PDF contains text that cannot be mapped to Unicode (meaning it is readable to human eyes but copy-pastes as gibberish). In these cases it recommends ``--force-ocr`` to make the text searchable. +- Added three new **experimental** features to improve OCR quality in + certain conditions. The name, syntax and behavior of these arguments + is subject to change. They may also be incompatible with some other + features. - - PDFs containing vector objects are now rendered at more appropriate resolution for OCR. + - ``--remove-vectors`` which strips out vector graphics. This can + improve OCR quality since OCR will not search artwork for readable + text; however, it currently removes "text as curves" as well. + - ``--mask-barcodes`` to detect and suppress barcodes in files. We + have observed that barcodes can interfere with OCR because they + are "text-like" but not actually textual. + - ``--threshold`` which uses a more sophisticated thresholding + algorithm than is currently in use in Tesseract OCR. This works + around a `known issue in Tesseract + 4.0 `__ + with dark text on bright backgrounds. - - We now exit with an error for PDFs that contain Adobe LiveCycle Designer's dynamic XFA forms. Currently the open source community does not have tools to work with these files. - - - OCRmyPDF now warns when a PDF that contains Adobe AcroForms, since such files probably do not need OCR. It can work with these files. - -- Added three new **experimental** features to improve OCR quality in certain conditions. The name, syntax and behavior of these arguments is subject to change. They may also be incompatible with some other features. - - - ``--remove-vectors`` which strips out vector graphics. This can improve OCR quality since OCR will not search artwork for readable text; however, it currently removes "text as curves" as well. - - - ``--mask-barcodes`` to detect and suppress barcodes in files. We have observed that barcodes can interfere with OCR because they are "text-like" but not actually textual. - - - ``--threshold`` which uses a more sophisticated thresholding algorithm than is currently in use in Tesseract OCR. This works around a `known issue in Tesseract 4.0 `_ with dark text on bright backgrounds. - -- Fixed an issue where an error message was not reported when the installed Ghostscript was very old. - -- The PDF optimizer now saves files with object streams enabled when the optimization level is ``--optimize 1`` or higher (the default). This makes files a little bit smaller, but requires PDF 1.5. PDF 1.5 was first released in 2003 and is broadly supported by PDF viewers, but some rudimentary PDF parsers such as PyPDF2 do not understand object streams. You can use the command line tool ``qpdf --object-streams=disable`` or `pikepdf `_ library to remove them. - -- New dependency: pdfminer.six 20181108. Note this is a fork of the Python 2-only pdfminer. - -- Deprecation notice: At the end of 2018, we will be ending support for Python 3.5 and Tesseract 3.x. OCRmyPDF v7 will continue to work with older versions. +- Fixed an issue where an error message was not reported when the + installed Ghostscript was very old. +- The PDF optimizer now saves files with object streams enabled when + the optimization level is ``--optimize 1`` or higher (the default). + This makes files a little bit smaller, but requires PDF 1.5. PDF 1.5 + was first released in 2003 and is broadly supported by PDF viewers, + but some rudimentary PDF parsers such as PyPDF2 do not understand + object streams. You can use the command line tool + ``qpdf --object-streams=disable`` or + `pikepdf `__ library to remove + them. +- New dependency: pdfminer.six 20181108. Note this is a fork of the + Python 2-only pdfminer. +- Deprecation notice: At the end of 2018, we will be ending support for + Python 3.5 and Tesseract 3.x. OCRmyPDF v7 will continue to work with + older versions. v7.2.1 ------- - -- Fix compatibility with an API change in pikepdf 0.3.5. - -- A kludge to support Leptonica versions older than 1.72 in the test suite was dropped. Older versions of Leptonica are likely still compatible. The only impact is that a portion of the test suite will be skipped. +====== +- Fix compatibility with an API change in pikepdf 0.3.5. +- A kludge to support Leptonica versions older than 1.72 in the test + suite was dropped. Older versions of Leptonica are likely still + compatible. The only impact is that a portion of the test suite will + be skipped. v7.2.0 ------- +====== **Lossy JBIG2 behavior change** -A user reported that ocrmypdf was in fact using JBIG2 in **lossy** compression mode. This was not the intended behavior. Users should `review the technical concerns with JBIG2 in lossy mode `_ and decide if this is a concern for their use case. +A user reported that ocrmypdf was in fact using JBIG2 in **lossy** +compression mode. This was not the intended behavior. Users should +`review the technical concerns with JBIG2 in lossy +mode `__ +and decide if this is a concern for their use case. -JBIG2 lossy mode does achieve higher compression ratios than any other monochrome compression technology; for large text documents the savings are considerable. JBIG2 lossless still gives great compression ratios and is a major improvement over the older CCITT G4 standard. +JBIG2 lossy mode does achieve higher compression ratios than any other +monochrome compression technology; for large text documents the savings +are considerable. JBIG2 lossless still gives great compression ratios +and is a major improvement over the older CCITT G4 standard. -Only users who have reviewed the concerns with JBIG2 in lossy mode should opt-in. As such, lossy mode JBIG2 is only turned on when the new argument ``--jbig2-lossy`` is issued. This is independent of the setting for ``--optimize``. +Only users who have reviewed the concerns with JBIG2 in lossy mode +should opt-in. As such, lossy mode JBIG2 is only turned on when the new +argument ``--jbig2-lossy`` is issued. This is independent of the setting +for ``--optimize``. Users who did not install an optional JBIG2 encoder are unaffected. @@ -209,930 +1019,1238 @@ Users who did not install an optional JBIG2 encoder are unaffected. **Other issues** -- When the image optimizer quantizes an image to 1 bit per pixel, it will now attempt to further optimize that image as CCITT or JBIG2, instead of keeping it in the "flate" encoding which is not efficient for 1 bpp images. (`#297 `_) - -- Images in PDFs that are used as soft masks (i.e. transparency masks or alpha channels) are now excluded from optimization. - -- Fixed handling of Tesseract 4.0-rc1 which now accepts invalid Tesseract configuration files, which broke the test suite. +- When the image optimizer quantizes an image to 1 bit per pixel, it + will now attempt to further optimize that image as CCITT or JBIG2, + instead of keeping it in the "flate" encoding which is not efficient + for 1 bpp images. + (`#297 `__) +- Images in PDFs that are used as soft masks (i.e. transparency masks + or alpha channels) are now excluded from optimization. +- Fixed handling of Tesseract 4.0-rc1 which now accepts invalid + Tesseract configuration files, which broke the test suite. v7.1.0 ------- +====== -- Improve the performance of initial text extraction, which is done to determine if a file contains existing text of some kind or not. On large files, this initial processing is now about 20x times faster. (`#299 `_) - -- pikepdf 0.3.3 is now required. - -- Fixed issue `#231 `_, a problem with JPEG2000 images where image metadata was only available inside the JPEG2000 file. - -- Fixed some additional Ghostscript 9.25 compatibility issues. - -- Improved handling of KeyboardInterrupt error messages. (`#301 `_) - -- README.md is now served in GitHub markdown instead of reStructuredText. +- Improve the performance of initial text extraction, which is done to + determine if a file contains existing text of some kind or not. On + large files, this initial processing is now about 20x times faster. + (`#299 `__) +- pikepdf 0.3.3 is now required. +- Fixed issue + `#231 `__, a + problem with JPEG2000 images where image metadata was only available + inside the JPEG2000 file. +- Fixed some additional Ghostscript 9.25 compatibility issues. +- Improved handling of KeyboardInterrupt error messages. + (`#301 `__) +- README.md is now served in GitHub markdown instead of + reStructuredText. v7.0.6 ------- - -- Blacklist Ghostscript 9.24, now that 9.25 is available and fixes many regressions in 9.24. +====== +- Blacklist Ghostscript 9.24, now that 9.25 is available and fixes many + regressions in 9.24. v7.0.5 ------- +====== -- Improve capability with Ghostscript 9.24, and enable the JPEG passthrough feature when this version in installed. - -- Ghostscript 9.24 lost the ability to set PDF title, author, subject and keyword metadata to Unicode strings. OCRmyPDF will set ASCII strings and warn when Unicode is suppressed. Other software may be used to update metadata. This is a short term work around. - -- PDFs generated by Kodak Capture Desktop, or generally PDFs that contain indirect references to null objects in their table of contents, would have an invalid table of contents after processing by OCRmyPDF that might interfere with other viewers. This has been fixed. - -- Detect PDFs generated by Adobe LiveCycle, which can only be displayed in Adobe Acrobat and Reader currently. When these are encountered, exit with an error instead of performing OCR on the "Please wait" error message page. +- Improve capability with Ghostscript 9.24, and enable the JPEG + passthrough feature when this version in installed. +- Ghostscript 9.24 lost the ability to set PDF title, author, subject + and keyword metadata to Unicode strings. OCRmyPDF will set ASCII + strings and warn when Unicode is suppressed. Other software may be + used to update metadata. This is a short term work around. +- PDFs generated by Kodak Capture Desktop, or generally PDFs that + contain indirect references to null objects in their table of + contents, would have an invalid table of contents after processing by + OCRmyPDF that might interfere with other viewers. This has been + fixed. +- Detect PDFs generated by Adobe LiveCycle, which can only be displayed + in Adobe Acrobat and Reader currently. When these are encountered, + exit with an error instead of performing OCR on the "Please wait" + error message page. v7.0.4 ------- +====== -- Fix exception thrown when trying to optimize a certain type of PNG embedded in a PDF with the ``-O2`` - -- Update to pikepdf 0.3.2, to gain support for optimizing some additional image types that were previously excluded from optimization (CMYK and grayscale). Fixes `#285 `_. +- Fix exception thrown when trying to optimize a certain type of PNG + embedded in a PDF with the ``-O2`` +- Update to pikepdf 0.3.2, to gain support for optimizing some + additional image types that were previously excluded from + optimization (CMYK and grayscale). Fixes + `#285 `__. v7.0.3 ------- +====== -- Fix issue `#284 `_, an error when parsing inline images that have are also image masks, by upgrading pikepdf to 0.3.1 +- Fix issue + `#284 `__, an error + when parsing inline images that have are also image masks, by + upgrading pikepdf to 0.3.1 v7.0.2 ------- +====== -- Fix a regression with ``--rotate-pages`` on pages that already had rotations applied. (`#279 `_) - -- Improve quality of page rotation in some cases by rasterizing a higher quality preview image. (`#281 `_) +- Fix a regression with ``--rotate-pages`` on pages that already had + rotations applied. + (`#279 `__) +- Improve quality of page rotation in some cases by rasterizing a + higher quality preview image. + (`#281 `__) v7.0.1 ------- +====== -- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images that have an alpha channel - -- Add forward compatibility for pikepdf 0.3.0 (unrelated to img2pdf) - -- Various documentation updates for v7.0.0 changes +- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images + that have an alpha channel +- Add forward compatibility for pikepdf 0.3.0 (unrelated to img2pdf) +- Various documentation updates for v7.0.0 changes v7.0.0 ------- +====== -- The core algorithm for combining OCR layers with existing PDF pages has been rewritten and improved considerably. PDFs are no longer split into single page PDFs for processing; instead, images are rendered and the OCR results are grafted onto the input PDF. The new algorithm uses less temporary disk space and is much more performant especially for large files. +- The core algorithm for combining OCR layers with existing PDF pages + has been rewritten and improved considerably. PDFs are no longer + split into single page PDFs for processing; instead, images are + rendered and the OCR results are grafted onto the input PDF. The new + algorithm uses less temporary disk space and is much more performant + especially for large files. +- New dependency: `pikepdf `__. + pikepdf is a powerful new Python PDF library driving the latest + OCRmyPDF features, built on the QPDF C++ library (libqpdf). +- New feature: PDF optimization with ``-O`` or ``--optimize``. After + OCR, OCRmyPDF will perform image optimizations relevant to OCR PDFs. -- New dependency: `pikepdf `_. pikepdf is a powerful new Python PDF library driving the latest OCRmyPDF features, built on the QPDF C++ library (libqpdf). + - If a JBIG2 encoder is available, then monochrome images will be + converted, with the potential for huge savings on large black and + white images, since JBIG2 is far more efficient than any other + monochrome (bi-level) compression. (All known US patents related + to JBIG2 have probably expired, but it remains the responsibility + of the user to supply a JBIG2 encoder such as + `jbig2enc `__. OCRmyPDF does not + implement JBIG2 encoding.) + - If ``pngquant`` is installed, OCRmyPDF will optionally use it to + perform lossy quantization and compression of PNG images. + - The quality of JPEGs can also be lowered, on the assumption that a + lower quality image may be suitable for storage after OCR. + - This image optimization component will eventually be offered as an + independent command line utility. + - Optimization ranges from ``-O0`` through ``-O3``, where ``0`` + disables optimization and ``3`` implements all options. ``1``, the + default, performs only safe and lossless optimizations. (This is + similar to GCC's optimization parameter.) The exact type of + optimizations performed will vary over time. -- New feature: PDF optimization with ``-O`` or ``--optimize``. After OCR, OCRmyPDF will perform image optimizations relevant to OCR PDFs. +- Small amounts of text in the margins of a page, such as watermarks, + page numbers, or digital stamps, will no longer prevent the rest of a + page from being OCRed when ``--skip-text`` is issued. This behavior + is based on a heuristic. +- Removed features - + If a JBIG2 encoder is available, then monochrome images will be converted, with the potential for huge savings on large black and white images, since JBIG2 is far more efficient than any other monochrome (bi-level) compression. (All known US patents related to JBIG2 have probably expired, but it remains the responsibility of the user to supply a JBIG2 encoder such as `jbig2enc `_. OCRmyPDF does not implement JBIG2 encoding.) + - The deprecated ``--pdf-renderer tesseract`` PDF renderer was + removed. + - ``-g``, the option to generate debug text pages, was removed + because it was a maintenance burden and only worked in isolated + cases. HOCR pages can still be previewed by running the + hocrtransform.py with appropriate settings. - + If ``pngquant`` is installed, OCRmyPDF will optionally use it to perform lossy quantization and compression of PNG images. +- Removed dependencies - + The quality of JPEGs can also be lowered, on the assumption that a lower quality image may be suitable for storage after OCR. + - ``PyPDF2`` + - ``defusedxml`` + - ``PyMuPDF`` - + This image optimization component will eventually be offered as an independent command line utility. +- The ``sandwich`` PDF renderer can be used with all supported versions + of Tesseract, including that those prior to v3.05 which don't support + ``-c textonly``. (Tesseract v4.0.0 is recommended and more + efficient.) +- ``--pdf-renderer auto`` option and the diagnostics used to select a + PDF renderer now work better with old versions, but may make + different decisions than past versions. +- If everything succeeds but PDF/A conversion fails, a distinct return + code is now returned (``ExitCode.pdfa_conversion_failed (10)``) where + this situation previously returned + ``ExitCode.invalid_output_pdf (4)``. The latter is now returned only + if there is some indication that the output file is invalid. +- Notes for downstream packagers - + Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables optimization and ``3`` implements all options. ``1``, the default, performs only safe and lossless optimizations. (This is similar to GCC's optimization parameter.) The exact type of optimizations performed will vary over time. - -- Small amounts of text in the margins of a page, such as watermarks, page numbers, or digital stamps, will no longer prevent the rest of a page from being OCRed when ``--skip-text`` is issued. This behavior is based on a heuristic. - -- Removed features - - + The deprecated ``--pdf-renderer tesseract`` PDF renderer was removed. - - + ``-g``, the option to generate debug text pages, was removed because it was a maintenance burden and only worked in isolated cases. HOCR pages can still be previewed by running the hocrtransform.py with appropriate settings. - -- Removed dependencies - - + ``PyPDF2`` - - + ``defusedxml`` - - + ``PyMuPDF`` - -- The ``sandwich`` PDF renderer can be used with all supported versions of Tesseract, including that those prior to v3.05 which don't support ``-c textonly``. (Tesseract v4.0.0 is recommended and more efficient.) - -- ``--pdf-renderer auto`` option and the diagnostics used to select a PDF renderer now work better with old versions, but may make different decisions than past versions. - -- If everything succeeds but PDF/A conversion fails, a distinct return code is now returned (``ExitCode.pdfa_conversion_failed (10)``) where this situation previously returned ``ExitCode.invalid_output_pdf (4)``. The latter is now returned only if there is some indication that the output file is invalid. - -- Notes for downstream packagers - - + There is also a new dependency on ``python-xmp-toolkit`` which in turn depends on ``libexempi3``. - - + It may be necessary to separately ``pip install pycparser`` to avoid `another Python 3.7 issue `_. + - There is also a new dependency on ``python-xmp-toolkit`` which in + turn depends on ``libexempi3``. + - It may be necessary to separately ``pip install pycparser`` to + avoid `another Python 3.7 + issue `__. v6.2.5 ------- +====== -- Disable a failing test due to Tesseract 4.0rc1 behavior change. Previously, Tesseract would exit with an error message if its configuration was invalid, and OCRmyPDF would intercept this message. Now Tesseract issues a warning, which OCRmyPDF v6.2.5 may relay or ignore. (In v7.x, OCRmyPDF will respond to the warning.) - -- This release branch no longer supports using the optional PyMuPDF installation, since it was removed in v7.x. - -- This release branch no longer supports macOS. macOS users should upgrade to v7.x. +- Disable a failing test due to Tesseract 4.0rc1 behavior change. + Previously, Tesseract would exit with an error message if its + configuration was invalid, and OCRmyPDF would intercept this message. + Now Tesseract issues a warning, which OCRmyPDF v6.2.5 may relay or + ignore. (In v7.x, OCRmyPDF will respond to the warning.) +- This release branch no longer supports using the optional PyMuPDF + installation, since it was removed in v7.x. +- This release branch no longer supports macOS. macOS users should + upgrade to v7.x. v6.2.4 ------- +====== -- Backport Ghostscript 9.25 compatibility fixes, which removes support for setting Unicode metadata -- Backport blacklisting Ghostscript 9.24 -- Older versions of Ghostscript are still supported +- Backport Ghostscript 9.25 compatibility fixes, which removes support + for setting Unicode metadata +- Backport blacklisting Ghostscript 9.24 +- Older versions of Ghostscript are still supported v6.2.3 ------- +====== -- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images that have an alpha channel -- This version will be included in Ubuntu 18.10 +- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images + that have an alpha channel +- This version will be included in Ubuntu 18.10 v6.2.2 ------- +====== -- Backport compatibility fixes for Python 3.7 and ruffus 2.7.0 from v7.0.0 -- Backport fix to ignore masks when deciding what colors are on a page -- Backport some minor improvements from v7.0.0: better argument validation and warnings about the Tesseract 4.0.0 ``--user-words`` regression +- Backport compatibility fixes for Python 3.7 and ruffus 2.7.0 from + v7.0.0 +- Backport fix to ignore masks when deciding what colors are on a page +- Backport some minor improvements from v7.0.0: better argument + validation and warnings about the Tesseract 4.0.0 ``--user-words`` + regression v6.2.1 ------- +====== -- Fix recent versions of Tesseract (after 4.0.0-beta1) not being detected as supporting the ``sandwich`` renderer (`#271 `_). +- Fix recent versions of Tesseract (after 4.0.0-beta1) not being + detected as supporting the ``sandwich`` renderer + (`#271 `__). v6.2.0 ------- - -- **Docker**: The Docker image ``ocrmypdf-tess4`` has been removed. The main Docker images, ``ocrmypdf`` and ``ocrmypdf-polyglot`` now use Ubuntu 18.04 as a base image, and as such Tesseract 4.0.0-beta1 is now the Tesseract version they use. There is no Docker image based on Tesseract 3.05 anymore. - -- Creation of PDF/A-3 is now supported. However, there is no ability to attach files to PDF/A-3. - -- Lists more reasons why the file size might grow. - -- Fix issue `#262 `_, ``--remove-background`` error on PDFs contained colormapped (paletted) images. - -- Fix another XMP metadata validation issue, in cases where the input file's creation date has no timezone and the creation date is not overridden. +====== +- **Docker**: The Docker image ``ocrmypdf-tess4`` has been removed. The + main Docker images, ``ocrmypdf`` and ``ocrmypdf-polyglot`` now use + Ubuntu 18.04 as a base image, and as such Tesseract 4.0.0-beta1 is + now the Tesseract version they use. There is no Docker image based on + Tesseract 3.05 anymore. +- Creation of PDF/A-3 is now supported. However, there is no ability to + attach files to PDF/A-3. +- Lists more reasons why the file size might grow. +- Fix issue + `#262 `__, + ``--remove-background`` error on PDFs contained colormapped + (paletted) images. +- Fix another XMP metadata validation issue, in cases where the input + file's creation date has no timezone and the creation date is not + overridden. v6.1.5 ------- - -- Fix issue `#253 `_, a possible division by zero when using the ``hocr`` renderer. - -- Fix incorrectly formatted ```` field inside XMP metadata for PDF/As. veraPDF flags this as a PDF/A validation failure. The error is caused the timezone and final digit of the seconds of modified time to be omitted, so at worst the modification time stamp is rounded to the nearest 10 seconds. +====== +- Fix issue + `#253 `__, a + possible division by zero when using the ``hocr`` renderer. +- Fix incorrectly formatted ```` field inside XMP + metadata for PDF/As. veraPDF flags this as a PDF/A validation + failure. The error is caused the timezone and final digit of the + seconds of modified time to be omitted, so at worst the modification + time stamp is rounded to the nearest 10 seconds. v6.1.4 ------- +====== -- Fix issue `#248 `_ ``--clean`` argument may remove OCR from left column of text on certain documents. We now set ``--layout none`` to suppress this. - -- The test cache was updated to reflect the change above. - -- Change test suite to accommodate Ghostscript 9.23's new ability to insert JPEGs into PDFs without transcoding. - -- XMP metadata in PDFs is now examined using ``defusedxml`` for safety. - -- If an external process exits with a signal when asked to report its version, we now print the system error message instead of suppressing it. This occurred when the required executable was found but was missing a shared library. - -- qpdf 7.0.0 or newer is now required as the test suite can no longer pass without it. +- Fix issue `#248 `__ + ``--clean`` argument may remove OCR from left column of text on + certain documents. We now set ``--layout none`` to suppress this. +- The test cache was updated to reflect the change above. +- Change test suite to accommodate Ghostscript 9.23's new ability to + insert JPEGs into PDFs without transcoding. +- XMP metadata in PDFs is now examined using ``defusedxml`` for safety. +- If an external process exits with a signal when asked to report its + version, we now print the system error message instead of suppressing + it. This occurred when the required executable was found but was + missing a shared library. +- qpdf 7.0.0 or newer is now required as the test suite can no longer + pass without it. Notes -~~~~~ - -- An apparent `regression in Ghostscript 9.23 `_ will cause some ocrmypdf output files to become invalid in rare cases; the workaround for the moment is to set ``--force-ocr``. +----- +- An apparent `regression in Ghostscript + 9.23 `__ will + cause some ocrmypdf output files to become invalid in rare cases; the + workaround for the moment is to set ``--force-ocr``. v6.1.3 ------- - -- Fix issue `#247 `_, ``/CreationDate`` metadata not copied from input to output. - -- A warning is now issued when Python 3.5 is used on files with a large page count, as this case is known to regress to single core performance. The cause of this problem is unknown. +====== +- Fix issue + `#247 `__, + ``/CreationDate`` metadata not copied from input to output. +- A warning is now issued when Python 3.5 is used on files with a large + page count, as this case is known to regress to single core + performance. The cause of this problem is unknown. v6.1.2 ------- - -- Upgrade to PyMuPDF v1.12.5 which includes a more complete fix to `#239 `_. - -- Add ``defusedxml`` dependency. +====== +- Upgrade to PyMuPDF v1.12.5 which includes a more complete fix to + `#239 `__. +- Add ``defusedxml`` dependency. v6.1.1 ------- - -- Fix text being reported as found on all pages if PyMuPDF is not installed. +====== +- Fix text being reported as found on all pages if PyMuPDF is not + installed. v6.1.0 ------- - -- PyMuPDF is now an optional but recommended dependency, to alleviate installation difficulties on platforms that have less access to PyMuPDF than the author anticipated. (For version 6.x only) install OCRmyPDF with ``pip install ocrmypdf[fitz]`` to use it to its full potential. - -- Fix ``FileExistsError`` that could occur if OCR timed out while it was generating the output file. (`#218 `_) - -- Fix table of contents/bookmarks all being redirected to page 1 when generating a PDF/A (with PyMuPDF). (Without PyMuPDF the table of contents is removed in PDF/A mode.) - -- Fix "RuntimeError: invalid key in dict" when table of contents/bookmarks titles contained the character ``)``. (`#239 `_) - -- Added a new argument ``--skip-repair`` to skip the initial PDF repair step if the PDF is already well-formed (because another program repaired it). +====== +- PyMuPDF is now an optional but recommended dependency, to alleviate + installation difficulties on platforms that have less access to + PyMuPDF than the author anticipated. (For version 6.x only) install + OCRmyPDF with ``pip install ocrmypdf[fitz]`` to use it to its full + potential. +- Fix ``FileExistsError`` that could occur if OCR timed out while it + was generating the output file. + (`#218 `__) +- Fix table of contents/bookmarks all being redirected to page 1 when + generating a PDF/A (with PyMuPDF). (Without PyMuPDF the table of + contents is removed in PDF/A mode.) +- Fix "RuntimeError: invalid key in dict" when table of + contents/bookmarks titles contained the character ``)``. + (`#239 `__) +- Added a new argument ``--skip-repair`` to skip the initial PDF repair + step if the PDF is already well-formed (because another program + repaired it). v6.0.0 ------- +====== -- The software license has been changed to GPLv3. Test resource files and some individual sources may have other licenses. +- The software license has been changed to GPLv3 [it has since changed again]. + Test resource files and some individual sources may have other licenses. +- OCRmyPDF now depends on + `PyMuPDF `__. + Including PyMuPDF is the primary reason for the change to GPLv3. +- Other backward incompatible changes -- OCRmyPDF now depends on `PyMuPDF `_. Including PyMuPDF is the primary reason for the change to GPLv3. - -- Other backward incompatible changes - - + The ``OCRMYPDF_TESSERACT``, ``OCRMYPDF_QPDF``, ``OCRMYPDF_GS`` and ``OCRMYPDF_UNPAPER`` environment variables are no longer used. Change ``PATH`` if you need to override the external programs OCRmyPDF uses. - - + The ``ocrmypdf`` package has been moved to ``src/ocrmypdf`` to avoid issues with accidental import. - - + The function ``ocrmypdf.exec.get_program`` was removed. - - + The deprecated module ``ocrmypdf.pageinfo`` was removed. - - + The ``--pdf-renderer tess4`` alias for ``sandwich`` was removed. - -- Fixed an issue where OCRmyPDF failed to detect existing text on pages, depending on how the text and fonts were encoded within the PDF. (`#233 `_, `#232 `_) - -- Fixed an issue that caused dramatic inflation of file sizes when ``--skip-text --output-type pdf`` was used. OCRmyPDF now removes duplicate resources such as fonts, images and other objects that it generates. (`#237 `_) - -- Improved performance of the initial page splitting step. Originally this step was not believed to be expensive and ran in a process. Large file testing revealed it to be a bottleneck, so it is now parallelized. On a 700 page file with quad core machine, this change saves about 2 minutes. (`#234 `_) - -- The test suite now includes a cache that can be used to speed up test runs across platforms. This also does not require computing checksums, so it's faster. (`#217 `_) + - The ``OCRMYPDF_TESSERACT``, ``OCRMYPDF_QPDF``, ``OCRMYPDF_GS`` and + ``OCRMYPDF_UNPAPER`` environment variables are no longer used. + Change ``PATH`` if you need to override the external programs + OCRmyPDF uses. + - The ``ocrmypdf`` package has been moved to ``src/ocrmypdf`` to + avoid issues with accidental import. + - The function ``ocrmypdf.exec.get_program`` was removed. + - The deprecated module ``ocrmypdf.pageinfo`` was removed. + - The ``--pdf-renderer tess4`` alias for ``sandwich`` was removed. +- Fixed an issue where OCRmyPDF failed to detect existing text on + pages, depending on how the text and fonts were encoded within the + PDF. (`#233 `__, + `#232 `__) +- Fixed an issue that caused dramatic inflation of file sizes when + ``--skip-text --output-type pdf`` was used. OCRmyPDF now removes + duplicate resources such as fonts, images and other objects that it + generates. + (`#237 `__) +- Improved performance of the initial page splitting step. Originally + this step was not believed to be expensive and ran in a process. + Large file testing revealed it to be a bottleneck, so it is now + parallelized. On a 700 page file with quad core machine, this change + saves about 2 minutes. + (`#234 `__) +- The test suite now includes a cache that can be used to speed up test + runs across platforms. This also does not require computing + checksums, so it's faster. + (`#217 `__) v5.7.0 ------- +====== -- Fixed an issue that caused poor CPU utilization on machines with more than 4 cores when running Tesseract 4. (Related to issue `#217 `_.) +- Fixed an issue that caused poor CPU utilization on machines with more + than 4 cores when running Tesseract 4. (Related to issue + `#217 `__.) +- The 'hocr' renderer has been improved. The 'sandwich' and 'tesseract' + renderers are still better for most use cases, but 'hocr' may be + useful for people who work with the PDF.js renderer in English/ASCII + languages. + (`#225 `__) -- The 'hocr' renderer has been improved. The 'sandwich' and 'tesseract' renderers are still better for most use cases, but 'hocr' may be useful for people who work with the PDF.js renderer in English/ASCII languages. (`#225 `_) - - + It now formats text in a matter that is easier for certain PDF viewers to select and extract copy and paste text. This should help macOS Preview and PDF.js in particular. - + The appearance of selected text and behavior of selecting text is improved. - + The PDF content stream now uses relative moves, making it more compact and easier for viewers to determine when two words on the same line. - + It can now deal with text on a skewed baseline. - + Thanks to @cforcey for the pull request, @jbreiden for many helpful suggestions, @ctbarbour for another round of improvements, and @acaloiaro for an independent review. + - It now formats text in a matter that is easier for certain PDF + viewers to select and extract copy and paste text. This should + help macOS Preview and PDF.js in particular. + - The appearance of selected text and behavior of selecting text is + improved. + - The PDF content stream now uses relative moves, making it more + compact and easier for viewers to determine when two words on the + same line. + - It can now deal with text on a skewed baseline. + - Thanks to @cforcey for the pull request, @jbreiden for many + helpful suggestions, @ctbarbour for another round of improvements, + and @acaloiaro for an independent review. v5.6.3 ------- - -- Suppress two debug messages that were too verbose +====== +- Suppress two debug messages that were too verbose v5.6.2 ------- - -- Development branch accidentally tagged as release. Do not use. +====== +- Development branch accidentally tagged as release. Do not use. v5.6.1 ------- - -- Fix issue `#219 `_: change how the final output file is created to avoid triggering permission errors when the output is a special file such as ``/dev/null`` -- Fix test suite failures due to a qpdf 8.0.0 regression and Python 3.5's handling of symlink -- The "encrypted PDF" error message was different depending on the type of PDF encryption. Now a single clear message appears for all types of PDF encryption. -- ocrmypdf is now in Homebrew. Homebrew users are advised to the version of ocrmypdf in the official homebrew-core formulas rather than the private tap. -- Some linting +====== +- Fix issue + `#219 `__: change + how the final output file is created to avoid triggering permission + errors when the output is a special file such as ``/dev/null`` +- Fix test suite failures due to a qpdf 8.0.0 regression and Python + 3.5's handling of symlink +- The "encrypted PDF" error message was different depending on the type + of PDF encryption. Now a single clear message appears for all types + of PDF encryption. +- ocrmypdf is now in Homebrew. Homebrew users are advised to the + version of ocrmypdf in the official homebrew-core formulas rather + than the private tap. +- Some linting v5.6.0 ------- - -- Fix issue `#216 `_: preserve "text as curves" PDFs without rasterizing file -- Related to the above, messages about rasterizing are more consistent -- For consistency versions minor releases will now get the trailing .0 they always should have had. +====== +- Fix issue + `#216 `__: preserve + "text as curves" PDFs without rasterizing file +- Related to the above, messages about rasterizing are more consistent +- For consistency versions minor releases will now get the trailing .0 + they always should have had. v5.5 ----- - -- Add new argument ``--max-image-mpixels``. Pillow 5.0 now raises an exception when images may be decompression bombs. This argument can be used to override the limit Pillow sets. -- Fix output page cropped when using the sandwich renderer and OCR is skipped on a rotated and image-processed page -- A warning is now issued when old versions of Ghostscript are used in cases known to cause issues with non-Latin characters -- Fix a few parameter validation checks for ``-output-type pdfa-1`` and ``pdfa-2`` +==== +- Add new argument ``--max-image-mpixels``. Pillow 5.0 now raises an + exception when images may be decompression bombs. This argument can + be used to override the limit Pillow sets. +- Fix output page cropped when using the sandwich renderer and OCR is + skipped on a rotated and image-processed page +- A warning is now issued when old versions of Ghostscript are used in + cases known to cause issues with non-Latin characters +- Fix a few parameter validation checks for ``-output-type pdfa-1`` and + ``pdfa-2`` v5.4.4 ------- - -- Fix issue `#181 `_: fix final merge failure for PDFs with more pages than the system file handle limit (``ulimit -n``) -- Fix issue `#200 `_: an uncommon syntax for formatting decimal numbers in a PDF would cause qpdf to issue a warning, which ocrmypdf treated as an error. Now this the warning is relayed. -- Fix an issue where intermediate PDFs would be created at version 1.3 instead of the version of the original file. It's possible but unlikely this had side effects. -- A warning is now issued when older versions of qpdf are used since issues like `#200 `_ cause qpdf to infinite-loop -- Address issue `#140 `_: if Tesseract outputs invalid UTF-8, escape it and print its message instead of aborting with a Unicode error -- Adding previously unlisted setup requirement, pytest-runner -- Update documentation: fix an error in the example script for Synology with Docker images, improved security guidance, advised ``pip install --user`` +====== +- Fix issue + `#181 `__: fix + final merge failure for PDFs with more pages than the system file + handle limit (``ulimit -n``) +- Fix issue + `#200 `__: an + uncommon syntax for formatting decimal numbers in a PDF would cause + qpdf to issue a warning, which ocrmypdf treated as an error. Now this + the warning is relayed. +- Fix an issue where intermediate PDFs would be created at version 1.3 + instead of the version of the original file. It's possible but + unlikely this had side effects. +- A warning is now issued when older versions of qpdf are used since + issues like + `#200 `__ cause + qpdf to infinite-loop +- Address issue + `#140 `__: if + Tesseract outputs invalid UTF-8, escape it and print its message + instead of aborting with a Unicode error +- Adding previously unlisted setup requirement, pytest-runner +- Update documentation: fix an error in the example script for Synology + with Docker images, improved security guidance, advised + ``pip install --user`` v5.4.3 ------- - -- If a subprocess fails to report its version when queried, exit cleanly with an error instead of throwing an exception -- Added test to confirm that the system locale is Unicode-aware and fail early if it's not -- Clarified some copyright information -- Updated pinned requirements.txt so the homebrew formula captures more recent versions +====== +- If a subprocess fails to report its version when queried, exit + cleanly with an error instead of throwing an exception +- Added test to confirm that the system locale is Unicode-aware and + fail early if it's not +- Clarified some copyright information +- Updated pinned requirements.txt so the homebrew formula captures more + recent versions v5.4.2 ------- - -- Fixed a regression from v5.4.1 that caused sidecar files to be created as empty files +====== +- Fixed a regression from v5.4.1 that caused sidecar files to be + created as empty files v5.4.1 ------- - -- Add workaround for Tesseract v4.00alpha crash when trying to obtain orientation and the latest language packs are installed +====== +- Add workaround for Tesseract v4.00alpha crash when trying to obtain + orientation and the latest language packs are installed v5.4 ----- - -- Change wording of a deprecation warning to improve clarity -- Added option to generate PDF/A-1b output if desired (``--output-type pdfa-1``); default remains PDF/A-2b generation -- Update documentation +==== +- Change wording of a deprecation warning to improve clarity +- Added option to generate PDF/A-1b output if desired + (``--output-type pdfa-1``); default remains PDF/A-2b generation +- Update documentation v5.3.3 ------- - -- Fixed missing error message that should occur when trying to force ``--pdf-renderer sandwich`` on old versions of Tesseract -- Update copyright information in test files -- Set system ``LANG`` to UTF-8 in Dockerfiles to avoid UTF-8 encoding errors +====== +- Fixed missing error message that should occur when trying to force + ``--pdf-renderer sandwich`` on old versions of Tesseract +- Update copyright information in test files +- Set system ``LANG`` to UTF-8 in Dockerfiles to avoid UTF-8 encoding + errors v5.3.2 ------- - -- Fixed a broken test case related to language packs +====== +- Fixed a broken test case related to language packs v5.3.1 ------- - -- Fixed wrong return code given for missing Tesseract language packs -- Fixed "brew audit" crashing on Travis when trying to auto-brew +====== +- Fixed wrong return code given for missing Tesseract language packs +- Fixed "brew audit" crashing on Travis when trying to auto-brew v5.3 ----- - -- Added ``--user-words`` and ``--user-patterns`` arguments which are forwarded to Tesseract OCR as words and regular expressions respective to use to guide OCR. Supplying a list of subject-domain words should assist Tesseract with resolving words. (`#165 `_) -- Using a non Latin-1 language with the "hocr" renderer now warns about possible OCR quality and recommends workarounds (`#176 `_) -- Output file path added to error message when that location is not writable (`#175 `_) -- Otherwise valid PDFs with leading whitespace at the beginning of the file are now accepted +==== +- Added ``--user-words`` and ``--user-patterns`` arguments which are + forwarded to Tesseract OCR as words and regular expressions + respective to use to guide OCR. Supplying a list of subject-domain + words should assist Tesseract with resolving words. + (`#165 `__) +- Using a non Latin-1 language with the "hocr" renderer now warns about + possible OCR quality and recommends workarounds + (`#176 `__) +- Output file path added to error message when that location is not + writable + (`#175 `__) +- Otherwise valid PDFs with leading whitespace at the beginning of the + file are now accepted v5.2 ----- - -- When using Tesseract 3.05.01 or newer, OCRmyPDF will select the "sandwich" PDF renderer by default, unless another PDF renderer is specified with the ``--pdf-renderer`` argument. The previous behavior was to select ``--pdf-renderer=hocr``. -- The "tesseract" PDF renderer is now deprecated, since it can cause problems with Ghostscript on Tesseract 3.05.00 -- The "tess4" PDF renderer has been renamed to "sandwich". "tess4" is now a deprecated alias for "sandwich". +==== +- When using Tesseract 3.05.01 or newer, OCRmyPDF will select the + "sandwich" PDF renderer by default, unless another PDF renderer is + specified with the ``--pdf-renderer`` argument. The previous behavior + was to select ``--pdf-renderer=hocr``. +- The "tesseract" PDF renderer is now deprecated, since it can cause + problems with Ghostscript on Tesseract 3.05.00 +- The "tess4" PDF renderer has been renamed to "sandwich". "tess4" is + now a deprecated alias for "sandwich". v5.1 ----- - -- Files with pages larger than 200" (5080 mm) in either dimension are now supported with ``--output-type=pdf`` with the page size preserved (in the PDF specification this feature is called UserUnit scaling). Due to Ghostscript limitations this is not available in conjunction with PDF/A output. +==== +- Files with pages larger than 200" (5080 mm) in either dimension are + now supported with ``--output-type=pdf`` with the page size preserved + (in the PDF specification this feature is called UserUnit scaling). + Due to Ghostscript limitations this is not available in conjunction + with PDF/A output. v5.0.1 ------- +====== -- Fixed issue `#169 `_, exception due to failure to create sidecar text files on some versions of Tesseract 3.04, including the jbarlow83/ocrmypdf Docker image +- Fixed issue + `#169 `__, + exception due to failure to create sidecar text files on some + versions of Tesseract 3.04, including the jbarlow83/ocrmypdf Docker + image v5.0 ----- +==== -- Backward incompatible changes +- Backward incompatible changes - + Support for Python 3.4 dropped. Python 3.5 is now required. - + Support for Tesseract 3.02 and 3.03 dropped. Tesseract 3.04 or newer is required. Tesseract 4.00 (alpha) is supported. - + The OCRmyPDF.sh script was removed. + - Support for Python 3.4 dropped. Python 3.5 is now required. + - Support for Tesseract 3.02 and 3.03 dropped. Tesseract 3.04 or + newer is required. Tesseract 4.00 (alpha) is supported. + - The OCRmyPDF.sh script was removed. -- Add a new feature, ``--sidecar``, which allows creating "sidecar" text files which contain the OCR results in plain text. These OCR text is more reliable than extracting text from PDFs. Closes `#126 `_. -- New feature: ``--pdfa-image-compression``, which allows overriding Ghostscript's lossy-or-lossless image encoding heuristic and making all images JPEG encoded or lossless encoded as desired. Fixes `#163 `_. -- Fixed issue `#143 `_, added ``--quiet`` to suppress "INFO" messages -- Fixed issue `#164 `_, a typo -- Removed the command line parameters ``-n`` and ``--just-print`` since they have not worked for some time (reported as Ubuntu bug `#1687308 `_) +- Add a new feature, ``--sidecar``, which allows creating "sidecar" + text files which contain the OCR results in plain text. These OCR + text is more reliable than extracting text from PDFs. Closes + `#126 `__. + +- New feature: ``--pdfa-image-compression``, which allows overriding + Ghostscript's lossy-or-lossless image encoding heuristic and making + all images JPEG encoded or lossless encoded as desired. Fixes + `#163 `__. + +- Fixed issue + `#143 `__, added + ``--quiet`` to suppress "INFO" messages + +- Fixed issue + `#164 `__, a typo + +- Removed the command line parameters ``-n`` and ``--just-print`` since + they have not worked for some time (reported as Ubuntu bug + `#1687308 `__) v4.5.6 ------- +====== -- Fixed issue `#156 `_, 'NoneType' object has no attribute 'getObject' on pages with no optional /Contents record. This should resolve all issues related to pages with no /Contents record. -- Fixed issue `#158 `_, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed. -- Fixed issue `#160 `_, exception thrown on certain invalid arguments instead of error message +- Fixed issue + `#156 `__, + 'NoneType' object has no attribute 'getObject' on pages with no + optional /Contents record. This should resolve all issues related to + pages with no /Contents record. +- Fixed issue + `#158 `__, ocrmypdf + now stops and terminates if Ghostscript fails on an intermediate + step, as it is not possible to proceed. +- Fixed issue + `#160 `__, + exception thrown on certain invalid arguments instead of error + message v4.5.5 ------- +====== -- Automated update of macOS homebrew tap -- Fixed issue `#154 `_, KeyError '/Contents' when searching for text on blank pages that have no /Contents record. Note: incomplete fix for this issue. +- Automated update of macOS homebrew tap +- Fixed issue + `#154 `__, KeyError + '/Contents' when searching for text on blank pages that have no + /Contents record. Note: incomplete fix for this issue. v4.5.4 ------- +====== -- Fix ``--skip-big`` raising an exception if a page contains no images (`#152 `_) (thanks to @TomRaz) -- Fix an issue where pages with no images might trigger "cannot write mode P as JPEG" (`#151 `_) +- Fix ``--skip-big`` raising an exception if a page contains no images + (`#152 `__) (thanks + to @TomRaz) +- Fix an issue where pages with no images might trigger "cannot write + mode P as JPEG" + (`#151 `__) v4.5.3 ------- +====== -- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling -- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly. -- Fixed an issue where the ``tess4`` renderer would duplicate content onto output pages if tesseract failed or timed out -- Fixed ``tess4`` renderer not recognized when lossless reconstruction is possible +- Added a workaround for Ghostscript 9.21 and probably earlier versions + would fail with the error message "VMerror -25", due to a Ghostscript + bug in XMP metadata handling +- High Unicode characters (U+10000 and up) are no longer accepted for + setting metadata on the command line, as Ghostscript may not handle + them correctly. +- Fixed an issue where the ``tess4`` renderer would duplicate content + onto output pages if tesseract failed or timed out +- Fixed ``tess4`` renderer not recognized when lossless reconstruction + is possible v4.5.2 ------- +====== -- Fix issue `#147 `_. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information. -- Make "using Tesseract 4.0" warning less ominous -- Set up machinery for homebrew OCRmyPDF tap +- Fix issue + `#147 `__. + ``--pdf-renderer tess4 --clean`` will produce an oversized page + containing the original image in the bottom left corner, due to loss + DPI information. +- Make "using Tesseract 4.0" warning less ominous +- Set up machinery for homebrew OCRmyPDF tap v4.5.1 ------- +====== -- Fix issue `#137 `_, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags +- Fix issue + `#137 `__, + proportions of images with a non-square pixel aspect ratio would be + distorted in output for ``--force-ocr`` and some other combinations + of flags v4.5 ----- +==== -- PDFs containing "Form XObjects" are now supported (issue `#134 `_; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing -- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail -- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases +- PDFs containing "Form XObjects" are now supported (issue + `#134 `__; PDF + reference manual 8.10), and images they contain are taken into + account when determining the resolution for rasterizing +- The Tesseract 4 Docker image no longer includes all languages, + because it took so long to build something would tend to fail +- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with + Tesseract 3.04 or lower due to issues with Ghostscript corrupting the + OCR text in these cases v4.4.2 ------- +====== -- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch +- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are + now based on Ubuntu 16.10 instead of Debian stretch - + This makes supporting the Tesseract 4 image easier - + This could be a disruptive change for any Docker users who built customized these images with their own changes, and made those changes in a way that depends on Debian and not Ubuntu + - This makes supporting the Tesseract 4 image easier + - This could be a disruptive change for any Docker users who built + customized these images with their own changes, and made those + changes in a way that depends on Debian and not Ubuntu -- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract 3.04, which was permitted in v4.4 and v4.4.1 but will not work +- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract + 3.04, which was permitted in v4.4 and v4.4.1 but will not work v4.4.1 ------- +====== -- To prevent a `TIFF output error `_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened -- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance -- Documentation improvements to describe the ``--tesseract-config`` feature -- Added test cases and fixed error handling for ``--tesseract-config`` -- Tweaks to setup.py to deal with issues in the v4.4 release +- To prevent a `TIFF output + error `__ caused + by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been + tightened +- The Tesseract 4.00 simultaneous process limit was increased from 1 to + 2, since it was observed that 1 lowers performance +- Documentation improvements to describe the ``--tesseract-config`` + feature +- Added test cases and fixed error handling for ``--tesseract-config`` +- Tweaks to setup.py to deal with issues in the v4.4 release v4.4 ----- +==== -- Tesseract 4.00 is now supported on an experimental basis. +- Tesseract 4.00 is now supported on an experimental basis. - + A new rendering option ``--pdf-renderer tess4`` exploits Tesseract 4's new text-only output PDF mode. See the documentation on PDF Renderers for details. - + The ``--tesseract-oem`` argument allows control over the Tesseract 4 OCR engine mode (tesseract's ``--oem``). Use ``--tesseract-oem 2`` to enforce the new LSTM mode. - + Fixed poor performance with Tesseract 4.00 on Linux + - A new rendering option ``--pdf-renderer tess4`` exploits Tesseract + 4's new text-only output PDF mode. See the documentation on PDF + Renderers for details. + - The ``--tesseract-oem`` argument allows control over the Tesseract + 4 OCR engine mode (tesseract's ``--oem``). Use + ``--tesseract-oem 2`` to enforce the new LSTM mode. + - Fixed poor performance with Tesseract 4.00 on Linux -- Fixed an issue that caused corruption of output to stdout in some cases -- Removed test for Pillow JPEG and PNG support, as the minimum supported version of Pillow now enforces this -- OCRmyPDF now tests that the intended destination file is writable before proceeding -- The test suite now requires ``pytest-helpers-namespace`` to run (but not install) -- Significant code reorganization to make OCRmyPDF re-entrant and improve performance. All changes should be backward compatible for the v4.x series. +- Fixed an issue that caused corruption of output to stdout in some + cases +- Removed test for Pillow JPEG and PNG support, as the minimum + supported version of Pillow now enforces this +- OCRmyPDF now tests that the intended destination file is writable + before proceeding +- The test suite now requires ``pytest-helpers-namespace`` to run (but + not install) +- Significant code reorganization to make OCRmyPDF re-entrant and + improve performance. All changes should be backward compatible for + the v4.x series. - + However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no Python API is available. Scripts should continue to use the command line interface. + - However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no + Python API is available. Scripts should continue to use the + command line interface. v4.3.5 ------- +====== -- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported. +- Update documentation to confirm Python 3.6.0 compatibility. No code + changes were needed, so many earlier versions are likely supported. v4.3.4 ------- +====== -- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images +- Fixed "decimal.InvalidOperation: quantize result has too many digits" + for high DPI images v4.3.3 ------- +====== -- Fixed PDF/A creation with Ghostscript 9.20 properly -- Fixed an exception on inline stencil masks with a missing optional parameter +- Fixed PDF/A creation with Ghostscript 9.20 properly +- Fixed an exception on inline stencil masks with a missing optional + parameter v4.3.2 ------- +====== -- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work) +- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix + did not actually work) v4.3.1 ------- +====== -- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker -- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n`` -- Ability to specify multiple languages for multilingual documents is now advertised in documentation -- Reduced the file sizes of some test resources -- Cleaned up debug output -- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed +- Fixed an issue where pages produced by the "hocr" renderer after a + Tesseract timeout would be rotated incorrectly if the input page was + rotated with a /Rotate marker +- Fixed a file handle leak in LeptonicaErrorTrap that would cause a + "too many open files" error for files around hundred pages of pages + long when ``--deskew`` or ``--remove-background`` or other Leptonica + based image processing features were in use, depending on the system + value of ``ulimit -n`` +- Ability to specify multiple languages for multilingual documents is + now advertised in documentation +- Reduced the file sizes of some test resources +- Cleaned up debug output +- Tesseract caching in test cases is now more cautious about false + cache hits and reproducing exact output, not that any problems were + observed v4.3 ----- +==== -- New feature ``--remove-background`` to detect and erase the background of color and grayscale images -- Better documentation -- Fixed an issue with PDFs that draw images when the raster stack depth is zero -- ocrmypdf can now redirect its output to stdout for use in a shell pipeline +- New feature ``--remove-background`` to detect and erase the + background of color and grayscale images +- Better documentation +- Fixed an issue with PDFs that draw images when the raster stack depth + is zero +- ocrmypdf can now redirect its output to stdout for use in a shell + pipeline - + This does not improve performance since temporary files are still used for buffering - + Some output validation is disabled in this mode + - This does not improve performance since temporary files are still + used for buffering + - Some output validation is disabled in this mode v4.2.5 ------- +====== -- Fixed an issue (`#100 `_) with PDFs that omit the optional /BitsPerComponent parameter on images -- Removed non-free file milk.pdf +- Fixed an issue + (`#100 `__) with + PDFs that omit the optional /BitsPerComponent parameter on images +- Removed non-free file milk.pdf v4.2.4 ------- +====== -- Fixed an error (`#90 `_) caused by PDFs that use stencil masks properly -- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI) +- Fixed an error + (`#90 `__) caused by + PDFs that use stencil masks properly +- Fixed handling of PDFs that try to draw images or stencil masks + without properly setting up the graphics state (such images are now + ignored for the purposes of calculating DPI) v4.2.3 ------- +====== -- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object -- Integrated a few fixes to simplify downstream packaging (Debian) +- Fixed an issue with PDFs that store page rotation (/Rotate) in an + indirect object +- Integrated a few fixes to simplify downstream packaging (Debian) - + The test suite no longer assumes it is installed - + If running Linux, skip a test that passes Unicode on the command line + - The test suite no longer assumes it is installed + - If running Linux, skip a test that passes Unicode on the command + line -- Added a test case to check explicit masks and stencil masks -- Added a test case for indirect objects and linearized PDFs -- Deprecated the OCRmyPDF.sh shell script +- Added a test case to check explicit masks and stencil masks +- Added a test case for indirect objects and linearized PDFs +- Deprecated the OCRmyPDF.sh shell script v4.2.2 ------- +====== -- Improvements to documentation +- Improvements to documentation v4.2.1 ------- +====== -- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort -- Implemented stdin streaming +- Fixed an issue where PDF pages that contained stencil masks would + report an incorrect DPI and cause Ghostscript to abort +- Implemented stdin streaming v4.2 ----- +==== -- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (`#15 `_) +- ocrmypdf will now try to convert single image files to PDFs if they + are provided as input + (`#15 `__) - + This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page. - + For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies) + - This is a basic convenience feature. It only supports a single + image and always makes the image fill the whole page. + - For better control over image to PDF conversion, use ``img2pdf`` + (one of ocrmypdf's dependencies) -- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation +- New argument ``--output-type {pdf|pdfa}`` allows disabling + Ghostscript PDF/A generation - + ``pdfa`` is the default, consistent with past behavior - + ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs - + ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes + - ``pdfa`` is the default, consistent with past behavior + - ``pdf`` provides a workaround for users concerned about the + increase in file size from Ghostscript forcing JBIG2 images to + CCITT and transcoding JPEGs + - ``pdf`` preserves as much as it can about the original file, + including problems that PDF/A conversion fixes -- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped) -- ``--force-ocr`` rasterizes pages even if they contain no images +- PDFs containing images with "non-square" pixel aspect ratios, such as + 200x100 DPI, are now handled and converted properly (fixing a bug + that caused to be cropped) +- ``--force-ocr`` rasterizes pages even if they contain no images - + supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text) - + supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered - + fixes issue `#82 `_ + - supports users who want to use OCRmyPDF to reconstruct text + information in PDFs with damaged Unicode maps (copy and paste text + does not match displayed text) + - supports reinterpreting PDFs where text was rendered as curves for + printing, and text needs to be recovered + - fixes issue + `#82 `__ -- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (`#79 `_) -- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty" +- Fixes an issue where, with certain settings, monochrome images in + PDFs would be converted to 8-bit grayscale, increasing file size + (`#79 `__) +- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of + (roughly) Ubuntu 14.04 LTS "trusty" - + Some Ubuntu "PPAs" (backports) are needed to make it work + - Some Ubuntu "PPAs" (backports) are needed to make it work -- Support for some older dependencies dropped +- Support for some older dependencies dropped - + Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports) - + Tesseract 3.03 or later is now required (available in Ubuntu trusty) + - Ghostscript 9.15 or later is now required (available in Ubuntu + trusty with backports) + - Tesseract 3.03 or later is now required (available in Ubuntu + trusty) -- Ghostscript now runs in "safer" mode where possible +- Ghostscript now runs in "safer" mode where possible v4.1.4 ------- +====== -- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images +- Bug fix: monochrome images with an ICC profile attached were + incorrectly converted to full color images if lossless reconstruction + was not possible due to other settings; consequence was increased + file size for these images v4.1.3 ------- +====== -- More helpful error message for PDFs with version 4 security handler -- Update usage instructions for Windows/Docker users -- Fix order of operations for matrix multiplication (no effect on most users) -- Add a few leptonica wrapper functions (no effect on most users) +- More helpful error message for PDFs with version 4 security handler +- Update usage instructions for Windows/Docker users +- Fix order of operations for matrix multiplication (no effect on most + users) +- Add a few leptonica wrapper functions (no effect on most users) v4.1.2 ------- +====== -- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license -- More helpful error message for an error related to certain types of malformed PDFs +- Replace IEC sRGB ICC profile with Debian's sRGB (from + icc-profiles-free) which is more compatible with the MIT license +- More helpful error message for an error related to certain types of + malformed PDFs v4.1 ----- +==== -- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold`` -- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time -- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages +- ``--rotate-pages`` now only rotates pages when reasonably confidence + in the orientation. This behavior can be adjusted with the new + argument ``--rotate-pages-threshold`` +- Fixed problems in error checking if ``unpaper`` is uninstalled or + missing at run-time +- Fixed problems with "RethrownJobError" errors during error handling + that suppressed the useful error messages v4.0.7 ------- +====== -- Minor correction to Ghostscript output settings +- Minor correction to Ghostscript output settings v4.0.6 ------- +====== -- Update install instructions -- Provide a sRGB profile instead of using Ghostscript's +- Update install instructions +- Provide a sRGB profile instead of using Ghostscript's v4.0.5 ------- +====== -- Remove some verbose debug messages from v4.0.4 -- Fixed temporary that wasn't being deleted -- DPI is now calculated correctly for cropped images, along with other image transformations -- Inline images are now checked during DPI calculation instead of rejecting the image +- Remove some verbose debug messages from v4.0.4 +- Fixed temporary that wasn't being deleted +- DPI is now calculated correctly for cropped images, along with other + image transformations +- Inline images are now checked during DPI calculation instead of + rejecting the image v4.0.4 ------- +====== -Released with verbose debug message turned on. Do not use. Skip to v4.0.5. +Released with verbose debug message turned on. Do not use. Skip to +v4.0.5. v4.0.3 ------- +====== New features -- Page orientations detected are now reported in a summary comment +- Page orientations detected are now reported in a summary comment Fixes -- Show stack trace if unexpected errors occur -- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than - abort the file -- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed - +- Show stack trace if unexpected errors occur +- Treat "too few characters" error message from Tesseract as a reason + to skip that page rather than abort the file +- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions + that have this fixed v4.0.2 ------- +====== Fixes - -- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting - orientation information -- Improved handling of Tesseract errors and crashes -- Fixed use of chmod on Docker that broke most test cases - +- Fixed compatibility with Tesseract 3.04.01 release, particularly its + different way of outputting orientation information +- Improved handling of Tesseract errors and crashes +- Fixed use of chmod on Docker that broke most test cases v4.0.1 ------- +====== Fixes - -- Fixed a KeyError if tesseract fails to find page orientation information - +- Fixed a KeyError if tesseract fails to find page orientation + information v4.0 ----- +==== New features -- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information - on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is - fairly reliable but some false positives occur especially if there is not much text to work with. (`#4 `_) -- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable - at image deskewing than unpaper. - +- Automatic page rotation (``-r``) is now available. It uses ignores + any prior rotation information on PDFs and sets rotation based on the + dominant orientation of detectable text. This feature is fairly + reliable but some false positives occur especially if there is not + much text to work with. + (`#4 `__) +- Deskewing is now performed using Leptonica instead of unpaper. + Leptonica is faster and more reliable at image deskewing than + unpaper. Fixes -- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly - if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag) -- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to - text layer if the page had been cropped such that its origin is not (0, 0) (`#49 `_) - +- Fixed an issue where lossless reconstruction could cause some pages + to be appear incorrectly if the page was rotated by the user in + Acrobat after being scanned (specifically if it a /Rotate tag) +- Fixed an issue where lossless reconstruction could misalign the + graphics layer with respect to text layer if the page had been + cropped such that its origin is not (0, 0) + (`#49 `__) Changes -- Logging output is now much easier to read -- ``--deskew`` is now performed by Leptonica instead of unpaper (`#25 `_) -- libffi is now required -- Some changes were made to the Docker and Travis build environments to support libffi -- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01, - the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00. - You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder - to correct the problem. - +- Logging output is now much easier to read +- ``--deskew`` is now performed by Leptonica instead of unpaper + (`#25 `__) +- libffi is now required +- Some changes were made to the Docker and Travis build environments to + support libffi +- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract + version is less than 3.04.01, the planned release that will include + fixes to an important OCR text rendering bug in Tesseract 3.04.00. + You can also manually install ./share/sharp2.ttf on top of pdf.ttf in + your Tesseract tessdata folder to correct the problem. v3.2.1 ------- +====== Changes -- Fixed issue `#47 `_ "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2 -- Tweaked the Dockerfiles - +- Fixed issue `#47 `__ + "convert() got and unexpected keyword argument 'dpi'" by upgrading to + img2pdf 0.2 +- Tweaked the Dockerfiles v3.2 ----- +==== New features -- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without - otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix - of vector and raster content would see the vector content preserved. Images may still be transcoded - during PDF/A conversion. (``--deskew`` and ``--clean-final`` disable this mode, necessarily.) -- New argument ``--tesseract-pagesegmode`` allows you to pass page segmentation arguments to Tesseract OCR. - This helps for two column text and other situations that confuse Tesseract. -- Added a new "polyglot" version of the Docker image, that generates Tesseract with all languages packs installed, - for the polyglots among us. It is much larger. +- Lossless reconstruction: when possible, OCRmyPDF will inject text + layers without otherwise manipulating the content and layout of a PDF + page. For example, a PDF containing a mix of vector and raster + content would see the vector content preserved. Images may still be + transcoded during PDF/A conversion. (``--deskew`` and + ``--clean-final`` disable this mode, necessarily.) +- New argument ``--tesseract-pagesegmode`` allows you to pass page + segmentation arguments to Tesseract OCR. This helps for two column + text and other situations that confuse Tesseract. +- Added a new "polyglot" version of the Docker image, that generates + Tesseract with all languages packs installed, for the polyglots among + us. It is much larger. Changes -- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation. - - +- JPEG transcoding quality is now 95 instead of the default 75. Bigger + file sizes for less degradation. v3.1.1 ------- +====== Changes -- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes +- Fixed bug that caused incorrect page size and DPI calculations on + documents with mixed page sizes v3.1 ----- +==== Changes -- Default output format is now PDF/A-2b instead of PDF/A-1b -- Python 3.5 and macOS El Capitan are now supported platforms - no changes were - needed to implement support -- Improved some error messages related to missing input files -- Fixed issue `#20 `_ - uppercase .PDF extension not accepted -- Fixed an issue where OCRmyPDF failed to text that certain pages contained previously OCR'ed text, - such as OCR text produced by Tesseract 3.04 -- Inserts /Creator tag into PDFs so that errors can be traced back to this project -- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the best PDF renderer. - Currently it always chooses the 'hocrtransform' renderer but that behavior may change. -- Set up Travis CI automatic integration testing +- Default output format is now PDF/A-2b instead of PDF/A-1b +- Python 3.5 and macOS El Capitan are now supported platforms - no + changes were needed to implement support +- Improved some error messages related to missing input files +- Fixed issue `#20 `__ + - uppercase .PDF extension not accepted +- Fixed an issue where OCRmyPDF failed to text that certain pages + contained previously OCR'ed text, such as OCR text produced by + Tesseract 3.04 +- Inserts /Creator tag into PDFs so that errors can be traced back to + this project +- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the + best PDF renderer. Currently it always chooses the 'hocrtransform' + renderer but that behavior may change. +- Set up Travis CI automatic integration testing v3.0 ----- +==== New features -- Easier installation with a Docker container or Python's ``pip`` package manager -- Eliminated many external dependencies, so it's easier to setup -- Now installs ``ocrmypdf`` to ``/usr/local/bin`` or equivalent for system-wide - access and easier typing -- Improved command line syntax and usage help (``--help``) -- Tesseract 3.03+ PDF page rendering can be used instead for better positioning - of recognized text (``--pdf-renderer tesseract``) -- PDF metadata (title, author, keywords) are now transferred to the - output PDF -- PDF metadata can also be set from the command line (``--title``, etc.) -- Automatic repairs malformed input PDFs if possible -- Added test cases to confirm everything is working -- Added option to skip extremely large pages that take too long to OCR and are - often not OCRable (e.g. large scanned maps or diagrams); other pages are still - processed (``--skip-big``) -- Added option to kill Tesseract OCR process if it seems to be taking too long on - a page, while still processing other pages (``--tesseract-timeout``) -- Less common colorspaces (CMYK, palette) are now supported by conversion to RGB -- Multiple images on the same PDF page are now supported +- Easier installation with a Docker container or Python's ``pip`` + package manager +- Eliminated many external dependencies, so it's easier to setup +- Now installs ``ocrmypdf`` to ``/usr/local/bin`` or equivalent for + system-wide access and easier typing +- Improved command line syntax and usage help (``--help``) +- Tesseract 3.03+ PDF page rendering can be used instead for better + positioning of recognized text (``--pdf-renderer tesseract``) +- PDF metadata (title, author, keywords) are now transferred to the + output PDF +- PDF metadata can also be set from the command line (``--title``, + etc.) +- Automatic repairs malformed input PDFs if possible +- Added test cases to confirm everything is working +- Added option to skip extremely large pages that take too long to OCR + and are often not OCRable (e.g. large scanned maps or diagrams); + other pages are still processed (``--skip-big``) +- Added option to kill Tesseract OCR process if it seems to be taking + too long on a page, while still processing other pages + (``--tesseract-timeout``) +- Less common colorspaces (CMYK, palette) are now supported by + conversion to RGB +- Multiple images on the same PDF page are now supported Changes -- New, robust rewrite in Python 3.4+ with ruffus_ pipelines -- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors -- OCR text is now rendered in the PDF as invisible text. Previous versions of OCRmyPDF - incorrectly rendered visible text with an image on top. -- All "tasks" in the pipeline can be executed in parallel on any - available CPUs, increasing performance -- The ``-o DPI`` argument has been phased out, in favor of ``--oversample DPI``, in - case we need ``-o OUTPUTFILE`` in the future -- Removed several dependencies, so it's easier to install. We no - longer use: +- New, robust rewrite in Python 3.4+ with + `ruffus `__ pipelines +- Now uses Ghostscript 9.14's improved color conversion model to + preserve PDF colors +- OCR text is now rendered in the PDF as invisible text. Previous + versions of OCRmyPDF incorrectly rendered visible text with an image + on top. +- All "tasks" in the pipeline can be executed in parallel on any + available CPUs, increasing performance +- The ``-o DPI`` argument has been phased out, in favor of + ``--oversample DPI``, in case we need ``-o OUTPUTFILE`` in the future +- Removed several dependencies, so it's easier to install. We no longer + use: - - GNU parallel_ - - ImageMagick_ - - Python 2.7 - - Poppler - - MuPDF_ tools - - shell scripts - - Java and JHOVE_ - - libxml2 + - GNU `parallel `__ + - `ImageMagick `__ + - Python 2.7 + - Poppler + - `MuPDF `__ tools + - shell scripts + - Java and `JHOVE `__ + - libxml2 -- Some new external dependencies are required or optional, compared to v2.x: +- Some new external dependencies are required or optional, compared to + v2.x: - - Ghostscript 9.14+ - - qpdf_ 5.0.0+ - - Unpaper_ 6.1 (optional) - - some automatically managed Python packages - -.. _ruffus: http://www.ruffus.org.uk/index.html -.. _parallel: https://www.gnu.org/software/parallel/ -.. _ImageMagick: http://www.imagemagick.org/script/index.php -.. _MuPDF: http://mupdf.com/docs/ -.. _qpdf: http://qpdf.sourceforge.net/ -.. _Unpaper: https://github.com/Flameeyes/unpaper -.. _JHOVE: http://jhove.sourceforge.net/ + - Ghostscript 9.14+ + - `qpdf `__ 5.0.0+ + - `Unpaper `__ 6.1 (optional) + - some automatically managed Python packages Release candidates^ -- rc9: +- rc9: - - fix issue `#118 `_: report error if ghostscript iccprofiles are missing - - fixed another issue related to `#111 `_: PDF rasterized to palette file - - add support image files with a palette - - don't try to validate PDF file after an exception occurs + - fix issue + `#118 `__: + report error if ghostscript iccprofiles are missing + - fixed another issue related to + `#111 `__: PDF + rasterized to palette file + - add support image files with a palette + - don't try to validate PDF file after an exception occurs -- rc8: +- rc8: - - fix issue `#111 `_: exception thrown if PDF is missing DocumentInfo dictionary + - fix issue + `#111 `__: + exception thrown if PDF is missing DocumentInfo dictionary -- rc7: +- rc7: - - fix error when installing direct from pip, "no such file 'requirements.txt'" + - fix error when installing direct from pip, "no such file + 'requirements.txt'" -- rc6: +- rc6: - - dropped libxml2 (Python lxml) since Python 3's internal XML parser is sufficient - - set up Docker container - - fix Unicode errors if recognized text contains Unicode characters and system locale is not UTF-8 + - dropped libxml2 (Python lxml) since Python 3's internal XML parser + is sufficient + - set up Docker container + - fix Unicode errors if recognized text contains Unicode characters + and system locale is not UTF-8 -- rc5: +- rc5: - - dropped Java and JHOVE in favour of qpdf - - improved command line error output - - additional tests and bug fixes - - tested on Ubuntu 14.04 LTS + - dropped Java and JHOVE in favour of qpdf + - improved command line error output + - additional tests and bug fixes + - tested on Ubuntu 14.04 LTS -- rc4: +- rc4: - - dropped MuPDF in favour of qpdf - - fixed some installer issues and errors in installation instructions - - improve performance: run Ghostscript with multithreaded rendering - - improve performance: use multiple cores by default - - bug fix: checking for wrong exception on process timeout + - dropped MuPDF in favour of qpdf + - fixed some installer issues and errors in installation + instructions + - improve performance: run Ghostscript with multithreaded rendering + - improve performance: use multiple cores by default + - bug fix: checking for wrong exception on process timeout -- rc3: skipping version number intentionally to avoid confusion with Tesseract -- rc2: first release for public testing to test-PyPI, Github -- rc1: testing release process +- rc3: skipping version number intentionally to avoid confusion with + Tesseract +- rc2: first release for public testing to test-PyPI, Github +- rc1: testing release process Compatibility notes -------------------- +=================== -- ``./OCRmyPDF.sh`` script is still available for now -- Stacking the verbosity option like ``-vvv`` is no longer supported - -- The configuration file ``config.sh`` has been removed. Instead, you can - feed a file to the arguments for common settings: +- ``./OCRmyPDF.sh`` script is still available for now +- Stacking the verbosity option like ``-vvv`` is no longer supported +- The configuration file ``config.sh`` has been removed. Instead, you + can feed a file to the arguments for common settings: :: - ocrmypdf input.pdf output.pdf @settings.txt + ocrmypdf input.pdf output.pdf @settings.txt where ``settings.txt`` contains *one argument per line*, for example: :: - -l - deu - --author - A. Merkel - --pdf-renderer - tesseract - + -l + deu + --author + A. Merkel + --pdf-renderer + tesseract Fixes - -- Handling of filenames containing spaces: fixed +- Handling of filenames containing spaces: fixed Notes and known issues -- Some dependencies may work with lower versions than tested, so try - overriding dependencies if they are "in the way" to see if they work. - -- ``--pdf-renderer tesseract`` will output files with an incorrect page size in Tesseract 3.03, - due to a bug in Tesseract. - -- PDF files containing "inline images" are not supported and won't be for the 3.0 release. Scanned - images almost never contain inline images. - +- Some dependencies may work with lower versions than tested, so try + overriding dependencies if they are "in the way" to see if they work. +- ``--pdf-renderer tesseract`` will output files with an incorrect page + size in Tesseract 3.03, due to a bug in Tesseract. +- PDF files containing "inline images" are not supported and won't be + for the 3.0 release. Scanned images almost never contain inline + images. v2.2-stable (2014-09-29) ------------------------- +======================== -OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions. +OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF +3.0+ is a fork that gradually replaced all shell scripts with Python +while maintaining the existing command line arguments. No one is +maintaining old versions. -For details on older versions, see the `final version of its release notes `_. +For details on older versions, see the `final version of its release +notes `__. diff --git a/docs/security.rst b/docs/security.rst deleted file mode 100644 index 1db8bf7e..00000000 --- a/docs/security.rst +++ /dev/null @@ -1,79 +0,0 @@ -PDF security issues -=================== - - OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware. - -Recognizing that many users have an interest in handling PDFs and applying OCR to PDFs they did not generate themselves, this article discusses the security implications of PDFs and how users can protect themselves. - -The disclaimer applies: this software has no warranties of any kind. - -PDFs may contain malware ------------------------- - -PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of pages long and references several annexes each of which are similar in length. PDFs can contain video, audio, XML, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors. - -In short, PDFs `may contain viruses `_. - -This `article `_ describes a high-paranoia method which allows potentially hostile PDFs to be viewed and rasterized safely in a disposable virtual machine. A trusted PDF created in this manner is converted to images and loses all information making it searchable and losing all compression. OCRmyPDF could be used restore searchability. - -How OCRmyPDF processes PDFs ---------------------------- - -OCRmyPDF must open and interpret your PDF in order to insert an OCR layer. First, it runs all PDFs through `pikepdf `_, a library based on `qpdf `_, a program that repairs PDFs with syntax errors. This is done because, in the author's experience, a significant number of PDFs in the wild especially those created by scanners are not well-formed files. qpdf makes it more likely that OCRmyPDF will succeed, but offers no security guarantees. qpdf is also used to split the PDF into single page PDFs. - -Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript `_ in ``-dSAFER`` mode. - -Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs. - -.. _ocr-service: - -Using OCRmyPDF online or as a service -------------------------------------- - -OCRmyPDF is not designed for use as a public web service where a malicious user could upload a chosen PDF. In particular, it is not necessarily secure against PDF malware or PDFs that cause denial of service. OCRmyPDF relies on Ghostscript, and therefore, if deployed online one should be prepared to comply with Ghostscript's Affero GPL license, OCRmyPDF's GPL license, and any other licenses. - -Setting aside these concerns, a side effect of OCRmyPDF is it may incidentally sanitize PDFs that contain certain types of malware. It runs ``qpdf`` to repair the PDF, which could correct malformed PDF structures that are part of an attack. When PDF/A output is selected (the default), the input PDF is partially reconstructed by Ghostscript. When ``--force-ocr`` is used, all pages are rasterized and reconverted to PDF, which could remove malware in embedded images. - -OCRmyPDF should be relatively safe to use in a trusted intranet, with some considerations: - -Limiting CPU usage -^^^^^^^^^^^^^^^^^^ - -OCRmyPDF will attempt to use all available CPUs and storage, so executing ``nice ocrmypdf`` or limiting the number of jobs with the ``-j`` argument may ensure the server remains available. Another option would be run OCRmyPDF jobs inside a Docker container, a virtual machine, or a cloud instance, which can impose its own limits on CPU usage and be terminated "from orbit" if it fails to complete. - -Temporary storage requirements -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -OCRmyPDF will use a large amount of temporary storage for its work, proportional to the total number of pixels needed to rasterize the PDF. The raster image of a 8.5×11" color page at 300 DPI takes 25 MB uncompressed; OCRmyPDF saves its intermediates as PNG, but that still means it requires about 9 MB per intermediate based on average compression ratios. Multiple intermediates per page are also required, depending on the command line given. A rule of thumb would be to allow 100 MB of temporary storage per page in a file – meaning that a small cloud servers or small VM partitions should be provisioned with plenty of extra space, if say, a 500 page file might be sent. - -To check temporary storage usage on actual files, run ``ocrmypdf -k ...`` which will preserve and print the path to temporary storage when the job is done. - -To change where temporary files are stored, change the ``TMPDIR`` environment variable for ocrmypdf's environment. (Python's ``tempfile.gettempdir()`` returns the root directory in which temporary files will be stored.) For example, one could redirect ``TMPDIR`` to a large RAM disk to avoid wear on HDD/SSD and potentially improve performance. On Amazon Web Services, ``TMPDIR`` can be set to `empheral storage `_. - -Timeouts -^^^^^^^^ - -To prevent excessively long OCR jobs consider setting ``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big`` is particularly helpful if your PDFs include documents such as reports on standard page sizes with large images attached - often large images are not worth OCR'ing anyway. - -Commercial alternatives -^^^^^^^^^^^^^^^^^^^^^^^ - -The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation. - -Abbyy Cloud OCR is a viable commercial alternative with a web services API. - - -Password protection, digital signatures and certification ---------------------------------------------------------- - -Password protected PDFs usually have two passwords, and owner and user password. When the user password is set to empty, PDF readers will open the file automatically and marked it as "(SECURED)". While not as reliable as a digital signature, this indicates that whoever set the password approved of the file at that time. When the user password is set, the document cannot be viewed without the password. - -Either way, OCRmyPDF does not remove passwords from PDFs and exits with an error on encountering them. - -``qpdf``, one of OCRmyPDF's dependencies, can remove passwords. If the owner and user password are set, a 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. - -After OCR is applied, password protection is not permitted on PDF/A documents but the file can be converted to regular PDF. - -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 to remove the signature image and apply it to other files. This practice offers no real security. - -Important documents can be digitally signed and certified to attest to their authorship. OCRmyPDF cannot do this. Open source tools such as pdfbox (Java) have this capability as does Adobe Acrobat. diff --git a/misc/batch.py b/misc/batch.py new file mode 100644 index 00000000..fcd0e5cf --- /dev/null +++ b/misc/batch.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Copyright 2016 findingorder: https://github.com/findingorder +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This script must be edited to meet your needs. + +import logging +import os +import sys + +import ocrmypdf + +# pylint: disable=logging-format-interpolation +# pylint: disable=logging-not-lazy + +script_dir = os.path.dirname(os.path.realpath(__file__)) +print(script_dir + '/batch.py: Start') + +if len(sys.argv) > 1: + start_dir = sys.argv[1] +else: + start_dir = '.' + +if len(sys.argv) > 2: + log_file = sys.argv[2] +else: + log_file = script_dir + '/ocr-tree.log' + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(message)s', + filename=log_file, + filemode='w', +) + +ocrmypdf.configure_logging(ocrmypdf.Verbosity.default) + +for dir_name, subdirs, file_list in os.walk(start_dir): + logging.info(dir_name + '\n') + os.chdir(dir_name) + for filename in file_list: + file_ext = os.path.splitext(filename)[1] + if file_ext == '.pdf': + full_path = dir_name + '/' + filename + print(full_path) + result = ocrmypdf.ocr(filename, filename, deskew=True) + if result == ocrmypdf.ExitCode.already_done_ocr: + print("Skipped document because it already contained text") + elif result == ocrmypdf.ExitCode.ok: + print("OCR complete") + logging.info(result) diff --git a/misc/completion/ocrmypdf.bash b/misc/completion/ocrmypdf.bash index 50d4a25e..b652769e 100644 --- a/misc/completion/ocrmypdf.bash +++ b/misc/completion/ocrmypdf.bash @@ -1,9 +1,58 @@ # ocrmypdf completion -*- shell-script -*- +# Copyright 2019 Frank Pille +# Copyright 2020 Alex Willner +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set -o errexit + _ocrmypdf() { local cur prev cword words split - _init_completion -s || return + + # Homebrew on Macs have version 1.3 of bash-completion which doesn't include - see #502 + if declare -F _init_completions >/dev/null 2>&1; then + _init_completion -s || return + else + COMPREPLY=() + _get_comp_words_by_ref cur prev words cword + fi + + if [[ $cur == -* ]]; then + COMPREPLY=( $( compgen -W '--language --image-dpi --output-type + --sidecar --version --jobs --quiet --verbose --title --author + --subject --keywords --rotate-pages --remove-background --deskew + --clean --clean-final --unpaper-args --oversample --remove-vectors + --threshold --force-ocr --skip-text --redo-ocr + --skip-big --jpeg-quality --png-quality --jbig2-lossy + --max-image-mpixels --tesseract-config --tesseract-pagesegmode + --help --tesseract-oem --pdf-renderer --tesseract-timeout + --rotate-pages-threshold --pdfa-image-compression --user-words + --user-patterns --keep-temporary-files --output-type + --no-progress-bar --pages --fast-web-view' \ + -- "$cur" ) ) + return + else + _filedir + return + fi case $prev in --version|-h|--help) @@ -49,39 +98,23 @@ _ocrmypdf() return ;; -v|--verbose) - COMPREPLY=( $( compgen -W '{1..9}' -- "$cur" ) ) # max level ? + COMPREPLY=( $( compgen -W '{0..2}' -- "$cur" ) ) # max level ? return ;; --tesseract-pagesegmode) COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) ) return ;; - --sidecar|--title|--author|--subject|--keywords|--unpaper-args) + --sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages|--fast-web-view) # argument required but no completions available return ;; esac $split && return - - if [[ $cur == -* ]]; then - COMPREPLY=( $( compgen -W '--language --image-dpi --output-type - --sidecar --version --jobs --quiet --verbose --title --author - --subject --keywords --rotate-pages --remove-background --deskew - --clean --clean-final --unpaper-args --oversample --remove-vectors - --mask-barcodes --threshold --force-ocr --skip-text --redo-ocr - --skip-big --jpeg-quality --png-quality --jbig2-lossy - --max-image-mpixels --tesseract-config --tesseract-pagesegmode - --help --tesseract-oem --pdf-renderer --tesseract-timeout - --rotate-pages-threshold --pdfa-image-compression --user-words - --user-patterns --keep-temporary-files --flowchart --output-type' \ - -- "$cur" ) ) - return - else - _filedir - return - fi } && complete -F _ocrmypdf ocrmypdf +set +o errexit + # ex: filetype=sh diff --git a/misc/completion/ocrmypdf.fish b/misc/completion/ocrmypdf.fish index 4c3c5d01..d085acdd 100644 --- a/misc/completion/ocrmypdf.fish +++ b/misc/completion/ocrmypdf.fish @@ -1,15 +1,34 @@ -complete -c ocrmypdf -l version -complete -c ocrmypdf -l help +# Copyright 2020 James R. Barlow +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. -complete -c ocrmypdf -l sidecar -r -d "write OCR to text file" -complete -c ocrmypdf -s q -l quiet +complete -c ocrmypdf -x -n '__fish_is_first_arg' -l version +complete -c ocrmypdf -x -n '__fish_is_first_arg' -s h -s "?" -l help + +complete -c ocrmypdf -r -l sidecar -d "write OCR to text file" +complete -c ocrmypdf -x -s q -l quiet complete -c ocrmypdf -s r -l rotate-pages -d "rotate pages to correct orientation" complete -c ocrmypdf -s d -l deskew -d "fix small horizontal alignment skew" complete -c ocrmypdf -s c -l clean -d "clean document images before OCR" complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep result" complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR" -complete -c ocrmypdf -l mask-barcodes -d "mask barcodes from OCR" complete -c ocrmypdf -l threshold -d "threshold images before OCR" complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text" @@ -18,8 +37,14 @@ complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)" -complete -c ocrmypdf -x -s l -l language -d 'language' -complete -c ocrmypdf -x -s l -l language -a '(tesseract --list-langs)' +function __fish_ocrmypdf_languages + set langs (tesseract --list-langs ^/dev/null) + set arr (string split '\n' $langs) + for lang in $arr[2..-1] + echo $lang + end +end +complete -c ocrmypdf -x -s l -l language -a '(__fish_ocrmypdf_languages)' -d "language" complete -c ocrmypdf -x -l image-dpi -d "assume this DPI if input image DPI is unknown" @@ -34,10 +59,11 @@ complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "se function __fish_ocrmypdf_pdf_renderer echo -e "auto\t"(_ "auto select PDF renderer") - echo -e "hocr\t"(_ "use hocr renderer") + echo -e "hocr\t"(_ "use hOCR renderer") + echo -e "hocrdebug\t"(_ "uses hOCR renderer in debug mode, showing recognized text") echo -e "sandwich\t"(_ "use sandwich renderer") end -complete -c ocrmypdf -x -l pdf-render -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options" +complete -c ocrmypdf -x -l pdf-renderer -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options" function __fish_ocrmypdf_optimize echo -e "0\t"(_ "do not optimize") @@ -47,8 +73,23 @@ function __fish_ocrmypdf_optimize end complete -c ocrmypdf -x -s O -l optimize -a '(__fish_ocrmypdf_optimize)' -d "select optimization level" +function __fish_ocrmypdf_verbose + echo -e "0\t"(_ "standard output messages") + echo -e "1\t"(_ "troubleshooting output messages") + echo -e "2\t"(_ "debugging output messages") +end +complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level" + +complete -c ocrmypdf -x -l no-progress-bar -d "disable the progress bar" + +function __fish_ocrmypdf_pdfa_compression + echo -e "auto\t"(_ "let Ghostscript decide how to compress images") + echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG") + echo -e "lossless\t"(_ "convert color and grayscale images to lossless (PNG)") +end +complete -c ocrmypdf -x -l pdfa-image-compression -a '(__fish_ocrmypdf_pdfa_compression)' -d "set PDF/A image compression options" + complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use" -complete -c ocrmypdf -x -s v -a '(seq 1 9)' complete -c ocrmypdf -x -l title -d "set metadata" complete -c ocrmypdf -x -l author -d "set metadata" complete -c ocrmypdf -x -l subject -d "set metadata" @@ -60,11 +101,39 @@ 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 jbig2-lossy -d "enable lossy JBIG2 (see docs)" 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 tesseract-config -d "set custom tesseract config file" -complete -c ocrmypdf -x -l tesseract-pagesegmode -d "set tesseract --psm" -complete -c ocrmypdf -x -l tesseract-oem -d "set tesseract --oem" + +function __fish_ocrmypdf_tesseract_pagesegmode + echo -e "0\t"(_ "orientation and script detection (OSD) only") + echo -e "1\t"(_ "automatic page segmentation with OSD") + echo -e "2\t"(_ "automatic page segmentation, but no OSD, or OCR") + echo -e "3\t"(_ "fully automatic page segmentation, but no OSD (default)") + echo -e "4\t"(_ "assume a single column of text of variable sizes") + echo -e "5\t"(_ "assume a single uniform block of vertically aligned text") + echo -e "6\t"(_ "assume a single uniform block of text") + echo -e "7\t"(_ "treat the image as a single text line") + echo -e "8\t"(_ "treat the image as a single word") + echo -e "9\t"(_ "treat the image as a single word in a circle") + echo -e "10\t"(_ "treat the image as a single character") + echo -e "11\t"(_ "sparse text - find as much text as possible in no particular order") + echo -e "12\t"(_ "sparse text with OSD") + echo -e "13\t"(_ "raw line - treat the image as a single text line") +end +complete -c ocrmypdf -x -l tesseract-pagesegmode -a '(__fish_ocrmypdf_tesseract_pagesegmode)' -d "set tesseract --psm" + +function __fish_ocrmypdf_tesseract_oem + echo -e "0\t"(_ "legacy engine only") + echo -e "1\t"(_ "neural nets LSTM engine only") + echo -e "2\t"(_ "legacy + LSTM engines") + echo -e "3\t"(_ "default, based on what is available") +end +complete -c ocrmypdf -x -l tesseract-oem -a '(__fish_ocrmypdf_tesseract_oem)' -d "set tesseract --oem" complete -c ocrmypdf -x -l tesseract-timeout -d "maximum number of seconds to wait for OCR" complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence" -complete -c ocrmypdf -x -l pdfa-image-compression -a 'auto jpeg lossless' -d "set PDF/A image compression options" -complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)" +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 -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)" diff --git a/misc/docker-compose.example.yml b/misc/docker-compose.example.yml new file mode 100644 index 00000000..9668b2b9 --- /dev/null +++ b/misc/docker-compose.example.yml @@ -0,0 +1,15 @@ +--- +version: "3.3" +services: + ocrmypdf: + restart: always + container_name: ocrmypdf + image: jbarlow83/ocrmypdf + volumes: + - "/media/scan:/input" + - "/mnt/scan:/output" + environment: + - OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0 + user: ":" + entrypoint: python3 + command: watcher.py diff --git a/misc/example_plugin.py b/misc/example_plugin.py new file mode 100644 index 00000000..cabb4ebe --- /dev/null +++ b/misc/example_plugin.py @@ -0,0 +1,84 @@ +# © 2020 James R Barlow: https://github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +An example of an OCRmyPDF plugin. + +This plugin adds two new command line arguments + --grayscale-ocr: converts the image to grayscale before performing OCR on it + (This is occasionally useful for images whose color confounds OCR. It only + affects the image shown to OCR. The image is not saved.) + --mono-page: converts pages all pages in the output file to black and white + +To use this from the command line: + ocrmypdf --plugin path/to/example_plugin.py --mono-page input.pdf output.pdf + +To use this as an API: + import ocrmypdf + ocrmypdf.ocr('input.pdf', 'output.pdf', + plugins=['path/to/example_plugin.py'], mono_page=True + ) +""" + +import logging + +from PIL import Image + +from ocrmypdf import hookimpl + +log = logging.getLogger(__name__) + + +@hookimpl +def add_options(parser): + parser.add_argument('--grayscale-ocr', action='store_true') + parser.add_argument('--mono-page', action='store_true') + + +@hookimpl +def prepare(options): + pass + + +@hookimpl +def validate(pdfinfo, options): + pass + + +@hookimpl +def filter_ocr_image(page, image): + if page.options.grayscale_ocr: + log.info("graying") + return image.convert('L') + return image + + +@hookimpl +def filter_page_image(page, image_filename): + if page.options.mono_page: + with Image.open(image_filename) as im: + im = im.convert('1') + im.save(image_filename) + return image_filename + else: + output = image_filename.with_suffix('.jpg') + with Image.open(image_filename) as im: + im.save(output) + return output diff --git a/misc/synology.py b/misc/synology.py new file mode 100644 index 00000000..6e294ce1 --- /dev/null +++ b/misc/synology.py @@ -0,0 +1,92 @@ +#!/bin/env python3 +# Copyright 2017 github.com/Enantiomerie +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This script must be edited to meet your needs. + +import logging +import os +import shutil +import subprocess +import sys +import time + +# pylint: disable=logging-format-interpolation +# pylint: disable=logging-not-lazy + +script_dir = os.path.dirname(os.path.realpath(__file__)) +timestamp = time.strftime("%Y-%m-%d-%H%M_") +log_file = script_dir + '/' + timestamp + 'ocrmypdf.log' +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(message)s', + filename=log_file, + filemode='w', +) + +if len(sys.argv) > 1: + start_dir = sys.argv[1] +else: + start_dir = '.' + +for dir_name, subdirs, file_list in os.walk(start_dir): + logging.info(dir_name) + os.chdir(dir_name) + for filename in file_list: + file_stem, file_ext = os.path.splitext(filename) + if file_ext != '.pdf': + continue + full_path = os.path.join(dir_name, filename) + timestamp_ocr = time.strftime("%Y-%m-%d-%H%M_OCR_") + filename_ocr = timestamp_ocr + file_stem + '.pdf' + # create string for pdf processing + # the script is processed as root user via chron + cmd = [ + 'docker', + 'run', + '--rm', + '-i', + 'jbarlow83/ocrmypdf', + '--deskew', + '-', + '-', + ] + logging.info(cmd) + full_path_ocr = os.path.join(dir_name, filename_ocr) + with open(filename, 'rb') as input_file, open( + full_path_ocr, 'wb' + ) as output_file: + proc = subprocess.run( + cmd, + stdin=input_file, + stdout=output_file, + stderr=subprocess.PIPE, + check=False, + text=True, + errors='ignore', + ) + logging.info(proc.stderr) + os.chmod(full_path_ocr, 0o664) + os.chmod(full_path, 0o664) + full_path_ocr_archive = sys.argv[2] + full_path_archive = sys.argv[2] + '/no_ocr' + shutil.move(full_path_ocr, full_path_ocr_archive) + shutil.move(full_path, full_path_archive) +logging.info('Finished.\n') diff --git a/misc/watcher.py b/misc/watcher.py new file mode 100644 index 00000000..68437878 --- /dev/null +++ b/misc/watcher.py @@ -0,0 +1,166 @@ +# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander +# Copyright (C) 2020 James R Barlow: https://github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json +import logging +import os +import sys +import time +from datetime import datetime +from pathlib import Path + +import pikepdf +from watchdog.events import PatternMatchingEventHandler +from watchdog.observers import Observer +from watchdog.observers.polling import PollingObserver + +import ocrmypdf + +# pylint: disable=logging-format-interpolation + +INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input') +OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output') +OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', '')) +ON_SUCCESS_DELETE = bool(os.getenv('OCR_ON_SUCCESS_DELETE', '')) +DESKEW = bool(os.getenv('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 = bool(os.getenv('OCR_USE_POLLING', '')) +LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO') +PATTERNS = ['*.pdf', '*.PDF'] + +log = logging.getLogger('ocrmypdf-watcher') + + +def get_output_dir(root, basename): + if OUTPUT_DIRECTORY_YEAR_MONTH: + today = datetime.today() + output_directory_year_month = ( + Path(root) / str(today.year) / f'{today.month:02d}' + ) + if not output_directory_year_month.exists(): + output_directory_year_month.mkdir(parents=True, exist_ok=True) + output_path = Path(output_directory_year_month) / basename + else: + output_path = Path(OUTPUT_DIRECTORY) / basename + return output_path + + +def wait_for_file_ready(file_path): + # This loop waits to make sure that the file is completely loaded on + # disk before attempting to read. Docker sometimes will publish the + # watchdog event before the file is actually fully on disk, causing + # pikepdf to fail. + + retries = 5 + while retries: + try: + pdf = pikepdf.open(file_path) + except (FileNotFoundError, pikepdf.PdfError) as e: + log.info(f"File {file_path} is not ready yet") + log.debug("Exception was", exc_info=e) + time.sleep(POLL_NEW_FILE_SECONDS) + retries -= 1 + else: + pdf.close() + return True + + return False + + +def execute_ocrmypdf(file_path): + file_path = Path(file_path) + output_path = get_output_dir(OUTPUT_DIRECTORY, file_path.name) + + log.info("-" * 20) + log.info(f'New file: {file_path}. Waiting until fully loaded...') + if not wait_for_file_ready(file_path): + log.info(f"Gave up waiting for {file_path} to become ready") + return + log.info(f'Attempting to OCRmyPDF to: {output_path}') + exit_code = ocrmypdf.ocr( + input_file=file_path, + output_file=output_path, + deskew=DESKEW, + **OCR_JSON_SETTINGS, + ) + if exit_code == 0 and ON_SUCCESS_DELETE: + log.info(f'OCR is done. Deleting: {file_path}') + file_path.unlink() + else: + log.info('OCR is done') + + +class HandleObserverEvent(PatternMatchingEventHandler): + def on_any_event(self, event): + if event.event_type in ['created']: + execute_ocrmypdf(event.src_path) + + +def main(): + ocrmypdf.configure_logging( + verbosity=( + ocrmypdf.Verbosity.default + if LOGLEVEL != 'DEBUG' + else ocrmypdf.Verbosity.debug + ), + manage_root_logger=True, + ) + log.setLevel(LOGLEVEL) + log.info( + f"Starting OCRmyPDF watcher with config:\n" + f"Input Directory: {INPUT_DIRECTORY}\n" + f"Output Directory: {OUTPUT_DIRECTORY}\n" + f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}" + ) + log.debug( + f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n" + f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n" + f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n" + f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n" + f"DESKEW: {DESKEW}\n" + f"ARGS: {OCR_JSON_SETTINGS}\n" + f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n" + f"USE_POLLING: {USE_POLLING}\n" + f"LOGLEVEL: {LOGLEVEL}" + ) + + if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS: + log.error('OCR_JSON_SETTINGS should not specify input file or output file') + sys.exit(1) + + handler = HandleObserverEvent(patterns=PATTERNS) + if USE_POLLING: + observer = PollingObserver() + else: + observer = Observer() + observer.schedule(handler, INPUT_DIRECTORY, recursive=True) + observer.start() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + + +if __name__ == "__main__": + main() diff --git a/.docker/webservice.py b/misc/webservice.py similarity index 99% rename from .docker/webservice.py rename to misc/webservice.py index 677561d5..ed2a0374 100644 --- a/.docker/webservice.py +++ b/misc/webservice.py @@ -23,21 +23,22 @@ to emphasize that SaaS deployments should make sure they comply with Ghostscript's license as well as OCRmyPDF's. """ +import os +import shlex +from subprocess import PIPE, run +from tempfile import TemporaryDirectory + from flask import ( Flask, Response, - flash, - request, - redirect, - url_for, abort, + flash, + redirect, + request, send_from_directory, + url_for, ) -from subprocess import run, PIPE -from tempfile import TemporaryDirectory from werkzeug.utils import secure_filename -import os -import shlex app = Flask(__name__) app.secret_key = "secret" diff --git a/pyproject.toml b/pyproject.toml index 8a3375ef..a28f55c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -py36 = true +target-version = ["py36", "py37", "py38"] skip-string-normalization = true include = '\.pyi?$' exclude = ''' @@ -28,5 +28,6 @@ exclude = ''' | docs | misc | \.egg-info + | src/ocrmypdf/lib/_leptonica.py )/ ''' diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 4faf987d..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -check-manifest >= 0.35 -twine >= 1.8.1 -coverage >= 4.5 -GitPython == 2.1.3 diff --git a/requirements/main.txt b/requirements/main.txt index 383d7189..a49cf9fa 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -1,13 +1,12 @@ # requirements.txt can be used to replicate the developer's build environment # setup.py lists a separate set of requirements that are looser to simplify # installation -chardet == 3.0.4 -cffi == 1.12.2 -img2pdf == 0.3.3 -pdfminer.six == 20181108 -pikepdf == 1.3.0 -Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin" -pycparser == 2.19 -python-xmp-toolkit == 2.0.1 -reportlab == 3.5.13 -ruffus == 2.8.1 +cffi == 1.14.5 +coloredlogs == 15.0 # technically optional +img2pdf == 0.4.0 +pdfminer.six == 20201018 +pikepdf == 2.10.0 +pluggy == 0.13.1 +Pillow == 8.1.2 +reportlab == 3.5.66 +tqdm == 4.59.0 diff --git a/requirements/test.txt b/requirements/test.txt index ad5ec593..72225e2d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,8 +1,6 @@ -pytest >= 4.4.1, < 5 -pytest-helpers-namespace >= 2019.1.8 -pytest-xdist == 1.28.0 -pytest-cov >= 2.6.1 -python-xmp-toolkit # requires apt-get install libexempi3 +pytest >= 6.0.0 +pytest-xdist >= 2.2.0 +pytest-cov >= 2.11.1 +python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3 # or brew install exempi -PyPDF2 >= 1.26.0 #PyMuPDF == 1.13.4 # optional diff --git a/requirements/watcher.txt b/requirements/watcher.txt new file mode 100644 index 00000000..660d7af4 --- /dev/null +++ b/requirements/watcher.txt @@ -0,0 +1 @@ +watchdog == 1.0.2 diff --git a/requirements/webservice.txt b/requirements/webservice.txt new file mode 100644 index 00000000..f6e3c4e6 --- /dev/null +++ b/requirements/webservice.txt @@ -0,0 +1 @@ +Flask >= 1, < 2 diff --git a/setup.cfg b/setup.cfg index 73ea8553..36545d66 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bdist_wheel] -python-tag = py35 +python-tag = py36 [aliases] test=pytest @@ -13,6 +13,10 @@ norecursedirs = lib .pc .git output cache resources testpaths = tests filterwarnings = ignore:.*XMLParser.*:DeprecationWarning +markers = + slow +addopts = + -n auto [isort] multi_line_output=3 @@ -20,6 +24,33 @@ include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 +known_first_party = ocrmypdf +known_third_party = PIL,_cffi_backend,cffi,flask,img2pdf,pdfminer,pikepdf,pkg_resources,pluggy,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug [metadata] license_file = LICENSE + +[coverage:paths] +source = + src/ocrmypdf + +[coverage:run] +branch = true +parallel = true +concurrency = multiprocessing + +[coverage:report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if False: + if __name__ == .__main__.: + if TYPE_CHECKING: diff --git a/setup.py b/setup.py index b5c959ea..e2342651 100644 --- a/setup.py +++ b/setup.py @@ -2,53 +2,21 @@ # -*- coding: utf-8 -*- # © 2015 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + from __future__ import print_function, unicode_literals import sys +from setuptools import find_packages, setup + if sys.version_info < (3, 6): print("Python 3.6 or newer is required", file=sys.stderr) sys.exit(1) -from setuptools import setup, find_packages -from subprocess import STDOUT, check_output, CalledProcessError -from collections.abc import Mapping -import re - -# pylint: disable=w0613 - - -command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '') -if command.startswith('install') or command in [ - 'check', - 'test', - 'nosetests', - 'easy_install', -]: - forced = '--force' in sys.argv - if forced: - print("The argument --force is deprecated. Please discontinue use.") - - -if 'upload' in sys.argv[1:]: - print('Use twine to upload the package - setup.py upload is insecure') - sys.exit(1) - tests_require = open('requirements/test.txt', encoding='utf-8').read().splitlines() @@ -64,20 +32,23 @@ setup( long_description_content_type='text/markdown', url='https://github.com/jbarlow83/OCRmyPDF', author='James R. Barlow', - author_email='jim@purplerock.ca', + author_email='james@purplerock.ca', packages=find_packages('src', exclude=["tests", "tests.*"]), package_dir={'': 'src'}, keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'], classifiers=[ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows :: Windows 10", "Operating System :: POSIX", "Operating System :: POSIX :: BSD", "Operating System :: POSIX :: Linux", @@ -88,28 +59,26 @@ setup( python_requires=' >= 3.6', setup_requires=[ # can be removed whenever we can drop pip 9 support 'cffi >= 1.9.1', # to build the leptonica module - 'pytest-runner', # to enable python setup.py test 'setuptools_scm', # so that version will work 'setuptools_scm_git_archive', # enable version from github tarballs ], use_scm_version={'version_scheme': 'post-release'}, cffi_modules=['src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'], install_requires=[ - 'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108 'cffi >= 1.9.1', # must be a setup and install requirement - 'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely - 'pdfminer.six == 20181108 ; sys_platform != "darwin"', - 'pikepdf >= 1.3.0, < 2', - 'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"', - # Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3 - # block 5.1.0, broken wheels - 'reportlab >= 3.3.0', # oldest released version with sane image handling - 'ruffus >= 2.7.0', + 'coloredlogs >= 14.0', # strictly optional + 'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely + 'pdfminer.six >= 20191110, != 20200720, <= 20201018', + "pikepdf >= 2.10.0", + 'Pillow >= 8.1.2', + 'pluggy >= 0.13.0, < 1.0', + 'reportlab >= 3.5.66', + 'setuptools', + 'tqdm >= 4', ], - extras_require={'pdfminer': ['pdfminer.six == 20181108']}, tests_require=tests_require, - entry_points={'console_scripts': ['ocrmypdf = ocrmypdf.__main__:run_pipeline']}, - package_data={'ocrmypdf': ['data/sRGB.icc']}, + entry_points={'console_scripts': ['ocrmypdf = ocrmypdf.__main__:run']}, + package_data={'ocrmypdf': ['data/sRGB.icc', 'py.typed']}, include_package_data=True, zip_safe=False, project_urls={ diff --git a/src/ocrmypdf/RELEASE.md b/src/ocrmypdf/RELEASE.md new file mode 100644 index 00000000..41a40e97 --- /dev/null +++ b/src/ocrmypdf/RELEASE.md @@ -0,0 +1,35 @@ +# Release checklist + +## Patch release + +- Check `pytest` + +- Update release notes + +## Minor release + +## Major release + +- Run `pre-commit autoupdate` + +- Check README.md + +- Check setup.py + + - Are classifiers up to date? + - Is `python_requires` correct? + - Python 3.6 is EOL on December 2021-12. Could drop support then. + - Can we tighten any `install_requires` dependencies? + +- Search for old version shims we can remove + + - "shim" + - ` pikepdf.__version__` + +- Search for deprecation: search all files for deprec*, etc. + +- Check requirements/* + +- Delete `tests/cache`, do `pytest --runslow`, and update cache. + +- Do `pytest --cov-report html` diff --git a/src/ocrmypdf/__init__.py b/src/ocrmypdf/__init__.py index a37d2658..80586658 100644 --- a/src/ocrmypdf/__init__.py +++ b/src/ocrmypdf/__init__.py @@ -1,46 +1,32 @@ # © 2017 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. -import pkg_resources -PROGRAM_NAME = 'ocrmypdf' +from pluggy import HookimplMarker as _HookimplMarker -# Official PEP 396 -__version__ = pkg_resources.get_distribution('ocrmypdf').version - -VERSION = __version__ - -from .exceptions import ( - ExitCode, +from ocrmypdf import helpers, hocrtransform, leptonica, pdfa, pdfinfo +from ocrmypdf._concurrent import Executor +from ocrmypdf._jobcontext import PageContext, PdfContext +from ocrmypdf._version import PROGRAM_NAME, __version__ +from ocrmypdf.api import Verbosity, configure_logging, ocr +from ocrmypdf.exceptions import ( BadArgsError, - PdfMergeFailedError, - MissingDependencyError, - UnsupportedImageFormatError, DpiError, - OutputFileAccessError, - PriorOcrFoundError, - InputFileError, - SubprocessOutputError, EncryptedPdfError, + ExitCode, + ExitCodeException, + InputFileError, + MissingDependencyError, + OutputFileAccessError, + PdfMergeFailedError, + PriorOcrFoundError, + SubprocessOutputError, TesseractConfigError, + UnsupportedImageFormatError, ) +from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence -from . import helpers -from . import hocrtransform -from . import leptonica -from . import pdfa -from . import pdfinfo +hookimpl = _HookimplMarker('ocrmypdf') diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index d60c243b..1046a50c 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -1,1149 +1,78 @@ #!/usr/bin/env python3 -# © 2015-17 James R. Barlow: github.com/jbarlow83 +# © 2015-19 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import argparse -import atexit import logging import os -import re +import signal import sys -import textwrap -from pathlib import Path -from tempfile import mkdtemp +from multiprocessing import set_start_method -import PIL -import ruffus.cmdline as cmdline -import ruffus.proxy_logger as proxy_logger -import ruffus.ruffus_exceptions as ruffus_exceptions - -from . import PROGRAM_NAME, VERSION -from . import exceptions as ocrmypdf_exceptions -from ._jobcontext import JobContext, JobContextManager, cleanup_working_files -from ._pipeline import build_pipeline -from ._unicodefun import verify_python3_env -from .exceptions import ( +from ocrmypdf import __version__ +from ocrmypdf._plugin_manager import get_parser_options_plugins +from ocrmypdf._sync import run_pipeline +from ocrmypdf._validation import check_closed_streams, check_options +from ocrmypdf.api import Verbosity, configure_logging +from ocrmypdf.exceptions import ( BadArgsError, ExitCode, - ExitCodeException, InputFileError, MissingDependencyError, - OutputFileAccessError, -) -from .exec import ( - ghostscript, - jbig2enc, - qpdf, - tesseract, - check_external_program, - unpaper, - pngquant, -) -from .helpers import available_cpu_count, is_file_writable, re_symlink -from .pdfa import file_claims_pdfa - -# ------------- -# External dependencies - -HOCR_OK_LANGS = frozenset(['eng', 'deu', 'spa', 'ita', 'por']) - - -def complain(message): - print(*textwrap.wrap(message), file=sys.stderr) - - -# -------- -# Critical environment tests - -verify_python3_env() - -# ------------- -# Parser - - -def numeric(basetype, min_=None, max_=None): - """Validator for numeric params""" - min_ = basetype(min_) if min_ is not None else None - max_ = basetype(max_) if max_ is not None else None - - def _numeric(string): - value = basetype(string) - if min_ is not None and value < min_ or max_ is not None and value > max_: - msg = "%r not in valid range %r" % (string, (min_, max_)) - raise argparse.ArgumentTypeError(msg) - return value - - _numeric.__name__ = basetype.__name__ - return _numeric - - -parser = argparse.ArgumentParser( - prog=PROGRAM_NAME, - fromfile_prefix_chars='@', - formatter_class=argparse.RawDescriptionHelpFormatter, - description="""\ -Generates a searchable PDF or PDF/A from a regular PDF. - -OCRmyPDF rasterizes each page of the input PDF, optionally corrects page -rotation and performs image processing, runs the Tesseract OCR engine on the -image, and then creates a PDF from the OCR information. -""", - epilog="""\ -OCRmyPDF attempts to keep the output file at about the same size. If a file -contains losslessly compressed images, and output file will be losslessly -compressed as well. - -PDF is a page description file that attempts to preserve a layout exactly. -A PDF can contain vector objects (such as text or lines) and raster objects -(images). A page might have multiple images. OCRmyPDF is prepared to deal -with the wide variety of PDFs that exist in the wild. - -When a PDF page contains text, OCRmyPDF assumes that the page has already -been OCRed or is a "born digital" page that should not be OCRed. The default -behavior is to exit in this case without producing a file. You can use the -option --skip-text to ignore pages with text, or --force-ocr to rasterize -all objects on the page and produce an image-only PDF as output. - - ocrmypdf --skip-text file_with_some_text_pages.pdf output.pdf - - ocrmypdf --force-ocr word_document.pdf output.pdf - -If you are concerned about long-term archiving of PDFs, use the default option ---output-type pdfa which converts the PDF to a standardized PDF/A-2b. This -converts images to sRGB colorspace, removes some features from the PDF such -as Javascript or forms. If you want to minimize the number of changes made to -your PDF, use --output-type pdf. - -If OCRmyPDF is given an image file as input, it will attempt to convert the -image to a PDF before processing. For more control over the conversion of -images to PDF, use the Python package img2pdf or other image to PDF software. - -For example, this command uses img2pdf to convert all .png files beginning -with the 'page' prefix to a PDF, fitting each image on A4-sized paper, and -sending the result to OCRmyPDF through a pipe. img2pdf is a dependency of -ocrmypdf so it is already installed. - - img2pdf --pagesize A4 page*.png | ocrmypdf - myfile.pdf - -Online documentation is located at: - https://ocrmypdf.readthedocs.io/en/latest/introduction.html - -""", ) -parser.add_argument( - 'input_file', - metavar="input_pdf_or_image", - help="PDF file containing the images to be OCRed (or '-' to read from " - "standard input)", -) -parser.add_argument( - 'output_file', - metavar="output_pdf", - help="Output searchable PDF file (or '-' to write to standard output). " - "Existing files will be ovewritten. If same as input file, the " - "input file will be updated only if processing is successful.", -) -parser.add_argument( - '-l', - '--language', - action='append', - help="Language(s) of the file to be OCRed (see tesseract --list-langs for " - "all language packs installed in your system). Use -l eng+deu for " - "multiple languages.", -) -parser.add_argument( - '--image-dpi', - metavar='DPI', - type=int, - help="For input image instead of PDF, use this DPI instead of file's.", -) -parser.add_argument( - '--output-type', - choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3'], - default='pdfa', - help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for " - "long term archiving (default, recommended) but may not suitable " - "for users who want their file altered as little as possible. 'pdfa' " - "also has problems with full Unicode text. 'pdf' attempts to " - "preserve file contents as much as possible. 'pdf-a1' creates a " - "PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a " - "PDF/A3-b file.", -) +log = logging.getLogger('ocrmypdf') -# Use null string '\0' as sentinel to indicate the user supplied no argument, -# since that is the only invalid character for filepaths on all platforms -# bool('\0') is True in Python -parser.add_argument( - '--sidecar', - nargs='?', - const='\0', - default=None, - metavar='FILE', - help="Generate sidecar text files that contain the same text recognized " - "by Tesseract. This may be useful for building a OCR text database. " - "If FILE is omitted, the sidecar file be named {output_file}.txt " - "If FILE is set to '-', the sidecar is written to stdout (a " - "convenient way to preview OCR quality). The output file and sidecar " - "may not both use stdout at the same time.", -) -parser.add_argument( - '--version', - action='version', - version=VERSION, - help="Print program version and exit", -) +def sigbus(*args): + raise InputFileError("Lost access to the input file") -jobcontrol = parser.add_argument_group("Job control options") -jobcontrol.add_argument( - '-j', - '--jobs', - metavar='N', - type=numeric(int, 0, 256), - help="Use up to N CPU cores simultaneously (default: use all).", -) -jobcontrol.add_argument( - '-q', '--quiet', action='store_true', help="Suppress INFO messages" -) -jobcontrol.add_argument( - '-v', - '--verbose', - const="+", - default=[], - nargs='?', - action="append", - help="Print more verbose messages for each additional verbose level. Use " - "`-v 1` typically for much more detailed logging. Higher numbers " - "are probably only useful in debugging.", -) -metadata = parser.add_argument_group( - "Metadata options", - "Set output PDF/A metadata (default: copy input document's metadata)", -) -metadata.add_argument( - '--title', type=str, help="Set document title (place multiple words in quotes)" -) -metadata.add_argument('--author', type=str, help="Set document author") -metadata.add_argument('--subject', type=str, help="Set document subject description") -metadata.add_argument('--keywords', type=str, help="Set document keywords") - -preprocessing = parser.add_argument_group( - "Image preprocessing options", - "Options to improve the quality of the final PDF and OCR", -) -preprocessing.add_argument( - '-r', - '--rotate-pages', - action='store_true', - help="Automatically rotate pages based on detected text orientation", -) -preprocessing.add_argument( - '--remove-background', - action='store_true', - help="Attempt to remove background from gray or color pages, setting it " - "to white ", -) -preprocessing.add_argument( - '-d', '--deskew', action='store_true', help="Deskew each page before performing OCR" -) -preprocessing.add_argument( - '-c', - '--clean', - action='store_true', - help="Clean pages from scanning artifacts before performing OCR, and send " - "the cleaned page to OCR, but do not include the cleaned page in " - "the output", -) -preprocessing.add_argument( - '-i', - '--clean-final', - action='store_true', - help="Clean page as above, and incorporate the cleaned image in the final " - "PDF. Might remove desired content.", -) -preprocessing.add_argument( - '--unpaper-args', - type=str, - default=None, - help="A quoted string of arguments to pass to unpaper. Requires --clean. " - "Example: --unpaper-args '--layout double'.", -) -preprocessing.add_argument( - '--oversample', - metavar='DPI', - type=numeric(int, 0, 5000), - default=0, - help="Oversample images to at least the specified DPI, to improve OCR " - "results slightly", -) -preprocessing.add_argument( - '--remove-vectors', - action='store_true', - help="EXPERIMENTAL. Mask out any vector objects in the PDF so that they " - "will not be included in OCR. This can eliminate false characters.", -) -preprocessing.add_argument( - '--mask-barcodes', - action='store_true', - help="EXPERIMENTAL. Mask out any barcodes that appear in the PDF so they are not " - "considered during OCR. Barcodes can introduce false characters into " - "OCR.", -) -preprocessing.add_argument( - '--threshold', - action='store_true', - help="EXPERIMENTAL. Threshold image to 1bpp before sending it to Tesseract for OCR. Can " - "improve OCR quality compared to Tesseract's thresholder.", -) - -ocrsettings = parser.add_argument_group("OCR options", "Control how OCR is applied") -ocrsettings.add_argument( - '-f', - '--force-ocr', - action='store_true', - help="Rasterize any text or vector objects on each page, apply OCR, and " - "save the rastered output (this rewrites the PDF)", -) -ocrsettings.add_argument( - '-s', - '--skip-text', - action='store_true', - help="Skip OCR on any pages that already contain text, but include the " - "page in final output; useful for PDFs that contain a mix of " - "images, text pages, and/or previously OCRed pages", -) -ocrsettings.add_argument( - '--redo-ocr', - action='store_true', - help="Attempt to detect and remove the hidden OCR layer from files that " - "were previously OCRed with OCRmyPDF or another program. Apply OCR " - "to text found in raster images. Existing visible text objects will " - "not be changed. If there is no existing OCR, OCR will be added.", -) -ocrsettings.add_argument( - '--skip-big', - type=numeric(float, 0, 5000), - metavar='MPixels', - help="Skip OCR on pages larger than the specified amount of megapixels, " - "but include skipped pages in final output", -) - -optimizing = parser.add_argument_group( - "Optimization options", "Control how the PDF is optimized after OCR" -) -optimizing.add_argument( - '-O', - '--optimize', - type=int, - choices=range(0, 4), - default=1, - help=( - "Control how PDF is optimized after processing:" - "0 - do not optimize; " - "1 - do safe, lossless optimizations (default); " - "2 - do some lossy optimizations; " - "3 - do aggressive lossy optimizations (including lossy JBIG2)" - ), -) -optimizing.add_argument( - '--jpeg-quality', - type=numeric(int, 0, 100), - default=0, - metavar='Q', - help=( - "Adjust JPEG quality level for JPEG optimization. " - "100 is best quality and largest output size; " - "1 is lowest quality and smallest output; " - "0 uses the default." - ), -) -optimizing.add_argument( - '--jpg-quality', - type=numeric(int, 0, 100), - default=0, - metavar='Q', - dest='jpeg_quality', - help=argparse.SUPPRESS, # Alias for --jpeg-quality -) -optimizing.add_argument( - '--png-quality', - type=numeric(int, 0, 100), - default=0, - metavar='Q', - help=( - "Adjust PNG quality level to use when quantizing PNGs. " - "Values have same meaning as with --jpeg-quality" - ), -) -optimizing.add_argument( - '--jbig2-lossy', - action='store_true', - help=( - "Enable JBIG2 lossy mode (better compression, not suitable for some " - "use cases - see documentation)." - ), -) -optimizing.add_argument( - '--jbig2-page-group-size', - type=numeric(int, 1, 10000), - default=0, - metavar='N', - # Adjust number of pages to consider at once for JBIG2 compression - help=argparse.SUPPRESS, -) - -advanced = parser.add_argument_group( - "Advanced", "Advanced options to control Tesseract's OCR behavior" -) -advanced.add_argument( - '--max-image-mpixels', - action='store', - type=numeric(float, 0), - metavar='MPixels', - help="Set maximum number of pixels to unpack before treating an image as a " - "decompression bomb", - default=128.0, -) -advanced.add_argument( - '--tesseract-config', - action='append', - metavar='CFG', - default=[], - help="Additional Tesseract configuration files -- see documentation", -) -advanced.add_argument( - '--tesseract-pagesegmode', - action='store', - type=int, - metavar='PSM', - choices=range(0, 14), - help="Set Tesseract page segmentation mode (see tesseract --help)", -) -advanced.add_argument( - '--tesseract-oem', - action='store', - type=int, - metavar='MODE', - choices=range(0, 4), - help=( - "Set Tesseract 4.0 OCR engine mode: " - "0 - original Tesseract only; " - "1 - neural nets LSTM only; " - "2 - Tesseract + LSTM; " - "3 - default." - ), -) -advanced.add_argument( - '--pdf-renderer', - choices=['auto', 'hocr', 'sandwich'], - default='auto', - help="Choose OCR PDF renderer - the default option is to let OCRmyPDF " - "choose. See documentation for discussion.", -) -advanced.add_argument( - '--tesseract-timeout', - default=180.0, - type=numeric(float, 0), - metavar='SECONDS', - help='Give up on OCR after the timeout, but copy the preprocessed page ' - 'into the final output', -) -advanced.add_argument( - '--rotate-pages-threshold', - default=14.0, - type=numeric(float, 0, 1000), - metavar='CONFIDENCE', - help="Only rotate pages when confidence is above this value (arbitrary " - "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( - '--user-words', - metavar='FILE', - help="Specify the location of the Tesseract user words file. This is a " - "list of words Tesseract should consider while performing OCR in " - "addition to its standard language dictionaries. This can improve " - "OCR quality especially for specialized and technical documents.", -) -advanced.add_argument( - '--user-patterns', - metavar='FILE', - help="Specify the location of the Tesseract user patterns file.", -) - -debugging = parser.add_argument_group( - "Debugging", "Arguments to help with troubleshooting and debugging" -) -debugging.add_argument( - '-k', - '--keep-temporary-files', - action='store_true', - help="Keep temporary files (helpful for debugging)", -) -debugging.add_argument( - '--flowchart', type=str, help="Generate the pipeline execution flowchart" -) - - -def check_options_languages(options, _log): - if not options.language: - options.language = ['eng'] # Enforce English hegemony - - # Support v2.x "eng+deu" language syntax - if '+' in options.language[0]: - options.language = options.language[0].split('+') - - languages = set(options.language) - if not languages.issubset(tesseract.languages()): - msg = ( - "The installed version of tesseract does not have language " - "data for the following requested languages: \n" - ) - for lang in languages - tesseract.languages(): - msg += lang + '\n' - raise MissingDependencyError(msg) - - -def check_options_output(options, log): - # We have these constraints to check for. - # 1. Ghostscript < 9.20 mangles multibyte Unicode - # 2. hocr doesn't work on non-Latin languages (so don't select it) - - languages = set(options.language) - is_latin = languages.issubset(HOCR_OK_LANGS) - - if options.pdf_renderer == 'hocr' and not is_latin: - msg = ( - "The 'hocr' PDF renderer is known to cause problems with one " - "or more of the languages in your document. Use " - "--pdf-renderer auto (the default) to avoid this issue." - ) - log.warning(msg) - - if ghostscript.version() < '9.20' and options.output_type != 'pdf' and not is_latin: - # https://bugs.ghostscript.com/show_bug.cgi?id=696874 - # Ghostscript < 9.20 fails to encode multibyte characters properly - msg = ( - "The installed version of Ghostscript does not work correctly " - "with the OCR languages you specified. Use --output-type pdf or " - "upgrade to Ghostscript 9.20 or later to avoid this issue." - ) - msg += f"Found Ghostscript {ghostscript.version()}" - log.warning(msg) - - # Decide on what renderer to use - if options.pdf_renderer == 'auto': - options.pdf_renderer = 'sandwich' - - if options.output_type == 'pdfa': - options.output_type = 'pdfa-2' - - if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19': - raise MissingDependencyError( - "--output-type pdfa-3 requires Ghostscript 9.19 or later" - ) - - lossless_reconstruction = False - if not any( - ( - options.deskew, - options.clean_final, - options.force_ocr, - options.remove_background, - ) - ): - lossless_reconstruction = True - options.lossless_reconstruction = lossless_reconstruction - - if not options.lossless_reconstruction and options.redo_ocr: - raise argparse.ArgumentError( - None, - "--redo-ocr is not currently compatible with --deskew, " - "--clean-final, and --remove-background", - ) - - -def check_options_sidecar(options, log): - if options.sidecar == '\0': - if options.output_file == '-': - raise argparse.ArgumentError( - None, - "--sidecar filename must be specified when output file is " "stdout.", - ) - options.sidecar = options.output_file + '.txt' - - -def check_options_preprocessing(options, log): - if options.clean_final: - options.clean = True - if options.unpaper_args and not options.clean: - raise argparse.ArgumentError(None, "--clean is required for --unpaper-args") - if options.clean: - check_external_program( - log=log, - program='unpaper', - package='unpaper', - version_checker=unpaper.version, - need_version='6.1', - required_for=['--clean, --clean-final'], - ) - try: - if options.unpaper_args: - options.unpaper_args = unpaper.validate_custom_args( - options.unpaper_args - ) - except Exception as e: - raise argparse.ArgumentError(None, str(e)) - - -def check_options_ocr_behavior(options, log): - exclusive_options = sum( - [ - (1 if opt else 0) - for opt in (options.force_ocr, options.skip_text, options.redo_ocr) - ] - ) - if exclusive_options >= 2: - raise argparse.ArgumentError( - None, "Error: choose only one of --force-ocr, --skip-text, --redo-ocr." - ) - - -def check_options_optimizing(options, log): - if options.optimize >= 2: - check_external_program( - log=log, - program='pngquant', - package='pngquant', - version_checker=pngquant.version, - need_version='2.0.1', - required_for='--optimize {2,3}', - ) - - if options.optimize >= 2: - # Although we use JBIG2 for optimize=1, don't nag about it unless the - # user is asking for more optimization - check_external_program( - log=log, - program='jbig2', - package='jbig2enc', - version_checker=jbig2enc.version, - need_version='0.28', - required_for='--optimize {2,3} | --jbig2-lossy', - recommended=True if not options.jbig2_lossy else False, - ) - - if options.optimize == 0 and any( - [options.jbig2_lossy, options.png_quality, options.jpeg_quality] - ): - log.warning( - "The arguments --jbig2-lossy, --png-quality, and --jpeg-quality " - "will be ignored because --optimize=0." - ) - - -def check_options_advanced(options, log): - if options.pdfa_image_compression != 'auto' and options.output_type.startswith( - 'pdfa' - ): - log.warning( - "--pdfa-image-compression argument has no effect when " - "--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'" - ) - if tesseract.v4() and (options.user_words or options.user_patterns): - log.warning('Tesseract 4.x ignores --user-words, so this has no effect') - - -def check_options_metadata(options, log): - import unicodedata - - docinfo = [options.title, options.author, options.keywords, options.subject] - for s in (m for m in docinfo if m): - for c in s: - if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000: - raise ValueError( - "One of the metadata strings contains " - "an unsupported Unicode character: '{}' (U+{})".format( - c, hex(ord(c))[2:].upper() - ) - ) - - -def check_options_pillow(options, log): - PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000) - if PIL.Image.MAX_IMAGE_PIXELS == 0: - PIL.Image.MAX_IMAGE_PIXELS = None - - -def check_options(options, log): - try: - check_options_languages(options, log) - check_options_metadata(options, log) - check_options_output(options, log) - check_options_sidecar(options, log) - check_options_preprocessing(options, log) - check_options_ocr_behavior(options, log) - check_options_optimizing(options, log) - check_options_advanced(options, log) - check_options_pillow(options, log) - except ValueError as e: - log.error(e) - sys.exit(ExitCode.bad_args) - except argparse.ArgumentError as e: - log.error(e) - sys.exit(ExitCode.bad_args) - except MissingDependencyError as e: - log.error(e) - sys.exit(ExitCode.missing_dependency) - - -# ---------- -# Logging - - -def logging_factory(logger_name, logger_args): - verbose = logger_args['verbose'] - quiet = logger_args['quiet'] - - root_logger = logging.getLogger(logger_name) - root_logger.setLevel(logging.DEBUG) - - handler = logging.StreamHandler(sys.stderr) - formatter_ = logging.Formatter("%(levelname)7s - %(message)s") - handler.setFormatter(formatter_) - if verbose: - handler.setLevel(logging.DEBUG) - elif quiet: - handler.setLevel(logging.WARNING) - else: - handler.setLevel(logging.INFO) - root_logger.addHandler(handler) - return root_logger - - -def cleanup_ruffus_error_message(msg): - msg = re.sub(r'\s+', r' ', msg) - msg = re.sub(r"\((.+?)\)", r'\1', msg) - msg = msg.strip() - return msg - - -def do_ruffus_exception(ruffus_five_tuple, options, log): - """Replace the elaborate ruffus stack trace with a user friendly - description of the error message that occurred.""" - exit_code = None - - _task_name, _job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple - - if isinstance(exc_name, type): - # ruffus is full of mystery... sometimes (probably when the process - # group leader is killed) exc_name is the class object of the exception, - # rather than a str. So reach into the object and get its name. - exc_name = exc_name.__name__ - - if exc_name.startswith('ocrmypdf.exceptions.'): - base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '') - exc_class = getattr(ocrmypdf_exceptions, base_exc_name) - exit_code = getattr(exc_class, 'exit_code', ExitCode.other_error) - try: - if isinstance(exc_value, exc_class): - exc_msg = str(exc_value) - elif isinstance(exc_value, str): - exc_msg = exc_value - else: - exc_msg = str(exc_class()) - except Exception: - exc_msg = "Unknown" - - if exc_name in ('builtins.SystemExit', 'SystemExit'): - match = re.search(r"\.(.+?)\)", exc_value) - exit_code_name = match.groups()[0] - exit_code = getattr(ExitCode, exit_code_name, 'other_error') - elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError': - log.error(cleanup_ruffus_error_message(exc_value)) - exit_code = ExitCode.input_file - elif exc_name in ('builtins.KeyboardInterrupt', 'KeyboardInterrupt'): - # We have to print in this case because the log daemon might be toast - print("Interrupted by user", file=sys.stderr) - exit_code = ExitCode.ctrl_c - elif exc_name == 'subprocess.CalledProcessError': - # It's up to the subprocess handler to report something useful - msg = "Error occurred while running this command:" - log.error(msg + '\n' + exc_value) - exit_code = ExitCode.child_process_error - elif exc_name.startswith('ocrmypdf.exceptions.'): - if exc_msg: - log.error(exc_msg) - elif exc_name == 'PIL.Image.DecompressionBombError': - msg = cleanup_ruffus_error_message(exc_value) - msg += ( - "\nUse the --max-image-mpixels argument to set increase the " - "maximum number of megapixels to accept." - ) - log.error(msg) - exit_code = ExitCode.input_file - - if exit_code is not None: - return exit_code - - if not options.verbose: - log.error(exc_stack) - return ExitCode.other_error - - -def traverse_ruffus_exception(exceptions, options, log): - """Traverse a RethrownJobError and output the exceptions - - Ruffus presents exceptions as 5 element tuples. The RethrownJobException - has a list of exceptions like - e.job_exceptions = [(5-tuple), (5-tuple), ...] - - ruffus < 2.7.0 had a bug with exception marshalling that would give - different output whether the main or child process raised the exception. - We no longer support this. - - Attempting to log the exception itself will re-marshall it to the logger - which is normally running in another process. It's better to avoid re- - marshalling. - - The exit code will be based on this, even if multiple exceptions occurred - at the same time.""" - - exit_codes = [] - for exc in exceptions: - exit_code = do_ruffus_exception(exc, options, log) - exit_codes.append(exit_code) - - return exit_codes[0] # Multiple codes are rare so take the first one - - -def check_closed_streams(options): - """Work around Python issue with multiprocessing forking on closed streams - - https://bugs.python.org/issue28326 - - Attempting to a fork/exec a new Python process when any of std{in,out,err} - are closed or not flushable for some reason may raise an exception. - Fix this by opening devnull if the handle seems to be closed. Do this - globally to avoid tracking places all places that fork. - - Seems to be specific to multiprocessing.Process not all Python process - forkers. - - The error actually occurs when the stream object is not flushable, - but replacing an open stream object that is not flushable with - /dev/null is a bad idea since it will create a silent failure. Replacing - a closed handle with /dev/null seems safe. - - """ - - if sys.version_info[0:3] >= (3, 6, 4): - return True # Issued fixed in Python 3.6.4+ - - if sys.stderr is None: - sys.stderr = open(os.devnull, 'w') - - if sys.stdin is None: - if options.input_file == '-': - print("Trying to read from stdin but stdin seems closed", file=sys.stderr) - return False - sys.stdin = open(os.devnull, 'r') - - if sys.stdout is None: - if options.output_file == '-': - # Can't replace stdout if the user is piping - # If this case can even happen, it must be some kind of weird - # stream. - print( - textwrap.dedent( - """\ - Output was set to stdout '-' but the stream attached to - stdout does not support the flush() system call. This - will fail.""" - ), - file=sys.stderr, - ) - return False - sys.stdout = open(os.devnull, 'w') - - return True - - -def log_page_orientations(pdfinfo, _log): - direction = {0: 'n', 90: 'e', 180: 's', 270: 'w'} - orientations = [] - for n, page in enumerate(pdfinfo): - angle = page.rotation or 0 - if angle != 0: - orientations.append('{0}{1}'.format(n + 1, direction.get(angle, ''))) - if orientations: - _log.info('Page orientations detected: ' + ' '.join(orientations)) - - -def preamble(_log): - _log.debug('ocrmypdf ' + VERSION) - - -def check_environ(options, _log): - old_envvars = ( - 'OCRMYPDF_TESSERACT', - 'OCRMYPDF_QPDF', - 'OCRMYPDF_GS', - 'OCRMYPDF_UNPAPER', - ) - for k in old_envvars: - if k in os.environ: - _log.warning( - textwrap.dedent( - f"""\ - OCRmyPDF no longer uses the environment variable {k}. - Change PATH to select alternate programs.""" - ) - ) - - -def check_input_file(options, _log, start_input_file): - if options.input_file == '-': - # stdin - _log.info('reading file from standard input') - with open(start_input_file, 'wb') as stream_buffer: - from shutil import copyfileobj - - copyfileobj(sys.stdin.buffer, stream_buffer) - else: - try: - re_symlink(options.input_file, start_input_file, _log) - except FileNotFoundError: - _log.error("File not found - " + options.input_file) - raise InputFileError() - - -def check_requested_output_file(options, _log): - if options.output_file == '-': - if sys.stdout.isatty(): - _log.error( - textwrap.dedent( - """\ - Output was set to stdout '-' but it looks like stdout - is connected to a terminal. Please redirect stdout to a - file.""" - ) - ) - raise BadArgsError() - elif not is_file_writable(options.output_file): - _log.error( - "Output file location (" - + options.output_file - + ") " - + "is not a writable file." - ) - raise OutputFileAccessError() - - -def report_output_file_size(options, _log, input_file, output_file): - try: - output_size = Path(output_file).stat().st_size - input_size = Path(input_file).stat().st_size - except FileNotFoundError: - return # Outputting to stream or something - ratio = output_size / input_size - if ratio < 1.35 or input_size < 25000: - return # Seems fine - - reasons = [] - image_preproc = { - 'deskew', - 'clean_final', - 'remove_background', - 'oversample', - 'force_ocr', - } - for arg in image_preproc: - attr = getattr(options, arg, None) - if not attr: - continue - reasons.append( - f"The argument --{arg.replace('_', '-')} was issued, causing transcoding." - ) - - if reasons: - explanation = "Possible reasons for this include:\n" + '\n'.join(reasons) + "\n" - else: - explanation = "No reason for this increase is known. Please report this issue." - - _log.warning( - textwrap.dedent( - f"""\ - The output file size is {ratio:.2f}× larger than the input file. - {explanation} - """ - ) - ) - - -def check_dependency_versions(options, log): - check_external_program( - log=log, - program='tesseract', - package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}, - version_checker=tesseract.version, - need_version='4.0.0', # using backport for Travis CI - ) - check_external_program( - log=log, - program='gs', - package='ghostscript', - version_checker=ghostscript.version, - need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports - ) - if ghostscript.version() == '9.24': - complain( - "Ghostscript 9.24 contains serious regressions and is not " - "supported. Please upgrade to Ghostscript 9.25 or use an older " - "version." - ) - return ExitCode.missing_dependency - check_external_program( - log=log, - program='qpdf', - package='qpdf', - version_checker=qpdf.version, - need_version='8.0.2', - ) - - -def run_pipeline(args=None): - options = parser.parse_args(args=args) - options.verbose_abbreviated_path = 1 - if os.environ.get('_OCRMYPDF_THREADS'): - options.use_threads = True +def run(args=None): + _parser, options, plugin_manager = get_parser_options_plugins(args=args) if not check_closed_streams(options): return ExitCode.bad_args - logger_args = {'verbose': options.verbose, 'quiet': options.quiet} + if hasattr(os, 'nice'): + os.nice(5) - _log, _log_mutex = proxy_logger.make_shared_logger_and_proxy( - logging_factory, __name__, logger_args + verbosity = options.verbose + if not os.isatty(sys.stderr.fileno()): + options.progress_bar = False + if options.quiet: + verbosity = Verbosity.quiet + options.progress_bar = False + configure_logging( + verbosity, + progress_bar_friendly=options.progress_bar, + manage_root_logger=True, + plugin_manager=plugin_manager, ) - preamble(_log) - check_options(options, _log) - check_dependency_versions(options, _log) - - # Any changes to options will not take effect for options that are already - # bound to function parameters in the pipeline. (For example - # options.input_file, options.pdf_renderer are already bound.) - if not options.jobs: - options.jobs = available_cpu_count() - - # Performance is improved by setting Tesseract to single threaded. In tests - # this gives better throughput than letting a smaller number of Tesseract - # jobs run multithreaded. Same story for pngquant. Tess <4 ignores this - # variable, but harmless to set if ignored. - os.environ.setdefault('OMP_THREAD_LIMIT', '1') - - check_environ(options, _log) - if os.environ.get('PYTEST_CURRENT_TEST'): - os.environ['_OCRMYPDF_TEST_INFILE'] = options.input_file - + log.debug('ocrmypdf %s', __version__) try: - work_folder = mkdtemp(prefix="com.github.ocrmypdf.") - options.history_file = os.path.join(work_folder, 'ruffus_history.sqlite') - start_input_file = os.path.join(work_folder, 'origin') - - check_input_file(options, _log, start_input_file) - check_requested_output_file(options, _log) - - manager = JobContextManager() - manager.register('JobContext', JobContext) # pylint: disable=no-member - manager.start() - - context = manager.JobContext() # pylint: disable=no-member - context.set_options(options) - context.set_work_folder(work_folder) - - build_pipeline(options, work_folder, _log, context) - atexit.register(cleanup_working_files, work_folder, options) - if hasattr(os, 'nice'): - os.nice(5) - cmdline.run(options) - except ruffus_exceptions.RethrownJobError as e: - if options.verbose: - _log.debug(str(e)) # stringify exception so logger doesn't have to - exceptions = e.job_exceptions - exitcode = traverse_ruffus_exception(exceptions, options, _log) - if exitcode is None: - _log.error("Unexpected ruffus exception: " + str(e)) - _log.error(repr(e)) - return ExitCode.other_error - return exitcode - except ExitCodeException as e: + check_options(options, plugin_manager) + except ValueError as e: + log.error(e) + return ExitCode.bad_args + except BadArgsError as e: + log.error(e) return e.exit_code - except Exception as e: - _log.error(str(e)) - return ExitCode.other_error + except MissingDependencyError as e: + log.error(e) + return ExitCode.missing_dependency - if options.flowchart: - _log.info(f"Flowchart saved to {options.flowchart}") - return ExitCode.ok - elif options.output_file == '-': - _log.info("Output sent to stdout") - elif os.path.samefile(options.output_file, os.devnull): - pass # Say nothing when sending to dev null - else: - if options.output_type.startswith('pdfa'): - pdfa_info = file_claims_pdfa(options.output_file) - if pdfa_info['pass']: - msg = f"Output file is a {pdfa_info['conformance']} (as expected)" - _log.info(msg) - else: - msg = f"Output file is okay but is not PDF/A (seems to be {pdfa_info['conformance']})" - _log.warning(msg) - return ExitCode.pdfa_conversion_failed - if not qpdf.check(options.output_file, _log): - _log.warning('Output file: The generated PDF is INVALID') - return ExitCode.invalid_output_pdf + if hasattr(signal, 'SIGBUS'): + signal.signal(signal.SIGBUS, sigbus) - report_output_file_size(options, _log, start_input_file, options.output_file) - - pdfinfo = context.get_pdfinfo() - if options.verbose: - from pprint import pformat - - _log.debug(pformat(pdfinfo)) - - log_page_orientations(pdfinfo, _log) - - return ExitCode.ok + result = run_pipeline(options=options, plugin_manager=plugin_manager) + return result if __name__ == '__main__': - sys.exit(run_pipeline()) + if sys.platform == 'darwin' and sys.version_info < (3, 8): + set_start_method('spawn') # see python bpo-33725 + sys.exit(run()) diff --git a/src/ocrmypdf/_concurrent.py b/src/ocrmypdf/_concurrent.py new file mode 100644 index 00000000..af505eb3 --- /dev/null +++ b/src/ocrmypdf/_concurrent.py @@ -0,0 +1,133 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import threading +from abc import ABC, abstractmethod +from typing import Callable, Iterable, Optional + + +def _task_noop(*_args, **_kwargs): + return + + +class NullProgressBar: + def __init__(self, **kwargs): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + return False + + def update(self, _arg=None): + return + + +class Executor(ABC): + pool_lock = threading.Lock() + pbar_class = NullProgressBar + + def __init__(self, *, pbar_class=None): + if pbar_class: + self.pbar_class = pbar_class + + def __call__( + self, + *, + use_threads: bool, + max_workers: int, + tqdm_kwargs: dict, + worker_initializer: Optional[Callable] = None, + task: Optional[Callable] = None, + task_arguments: Optional[Iterable] = None, + task_finished: Optional[Callable] = None, + ) -> None: + """ + Set up parallel execution and progress reporting. + + Args: + use_threads: If ``False``, the workload is the sort that will benefit from + running in a multiprocessing context (for example, it uses Python + heavily, and parallelizing it with threads is not expected to be + performant). + max_workers: The maximum number of workers that should be run. + tdqm_kwargs: Arguments to set up the progress bar. + worker_initializer: Called when a worker is initialized, in the worker's + execution context. If the child workers are processes, it must be + possible to marshall/pickle the worker initializer. + ``functools.partial`` can be used to bind parameters. + task: Called when the worker starts a new task, in the worker's execution + context. Must be possible to marshall to the worker. + task_finished: Called when a worker finishes a task, in the parent's + context. + task_arguments: An iterable that generates a group of parameters for each + task. This runs in the parent's context, but the parameters must be + marshallable to the worker. + """ + + if not task_arguments: + return # Nothing to do! + if not worker_initializer: + worker_initializer = _task_noop + if not task_finished: + task_finished = _task_noop + if not task: + task = _task_noop + + with self.pool_lock: + self._execute( + use_threads=use_threads, + max_workers=max_workers, + tqdm_kwargs=tqdm_kwargs, + worker_initializer=worker_initializer, + task=task, + task_arguments=task_arguments, + task_finished=task_finished, + ) + + @abstractmethod + def _execute( + self, + *, + use_threads: bool, + max_workers: int, + tqdm_kwargs: dict, + worker_initializer: Callable, + task: Callable, + task_arguments: Iterable, + task_finished: Callable, + ): + """Custom executors should override this method.""" + + +def setup_executor(plugin_manager) -> Executor: + pbar_class = plugin_manager.hook.get_progressbar_class() + return plugin_manager.hook.get_executor(progressbar_class=pbar_class) + + +class SerialExecutor(Executor): + """Implements a purely sequential executor using the parallel protocol. + + The current process/thread will be the worker that executes all tasks + in order. As such, ``worker_initializer`` will never be called. + """ + + def _execute( + self, + *, + use_threads: bool, + max_workers: int, + tqdm_kwargs: dict, + worker_initializer: Callable, + task: Callable, + task_arguments: Iterable, + task_finished: Callable, + ): # pylint: disable=unused-argument + with self.pbar_class(**tqdm_kwargs) as pbar: + for args in task_arguments: + result = task(args) + task_finished(result, pbar) diff --git a/src/ocrmypdf/_exec/__init__.py b/src/ocrmypdf/_exec/__init__.py new file mode 100644 index 00000000..36dc7182 --- /dev/null +++ b/src/ocrmypdf/_exec/__init__.py @@ -0,0 +1,8 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Manage third party executables""" diff --git a/src/ocrmypdf/_exec/ghostscript.py b/src/ocrmypdf/_exec/ghostscript.py new file mode 100644 index 00000000..5c357f1b --- /dev/null +++ b/src/ocrmypdf/_exec/ghostscript.py @@ -0,0 +1,270 @@ +# © 2017 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Interface to Ghostscript executable""" + +import logging +import os +import re +from io import BytesIO +from os import fspath +from pathlib import Path +from shutil import which +from subprocess import PIPE, CalledProcessError +from typing import Optional + +from PIL import Image + +from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError +from ocrmypdf.helpers import Resolution +from ocrmypdf.subprocess import get_version, run, run_polling_stderr + +log = logging.getLogger(__name__) + +missing_gs_error = """ +--------------------------------------------------------------------- +This error normally occurs when ocrmypdf find can't Ghostscript. +Please ensure Ghostscript is installed and its location is added to +the system PATH environment variable. + +For details see: + https://ocrmypdf.readthedocs.io/en/latest/installation.html +--------------------------------------------------------------------- +""" + +_gswin = None +if os.name == 'nt': + _gswin = which('gswin64c') + if not _gswin: + _gswin = which('gswin32c') + if not _gswin: + raise MissingDependencyError(missing_gs_error) + _gswin = Path(_gswin).stem + +GS = _gswin if _gswin else 'gs' +del _gswin + + +def version(): + return get_version(GS) + + +def jpeg_passthrough_available() -> bool: + """Returns True if the installed version of Ghostscript supports JPEG passthru + + Prior to 9.23, Ghostscript decoded and re-encoded JPEGs internally. In 9.23 + it gained the ability to keep JPEGs unmodified. However, the 9.23 + implementation was buggy and would deletes the last two bytes of images in + some cases, as reported here. + https://bugs.ghostscript.com/show_bug.cgi?id=699216 + + The issue was fixed for 9.24, hence that is the first version we consider + the feature available. (Ghostscript 9.24 has its own problems is blacklisted.) + """ + return version() >= '9.24' + + +def _gs_error_reported(stream) -> bool: + return True if re.search(r'error', stream, flags=re.IGNORECASE) else False + + +def rasterize_pdf( + input_file: os.PathLike, + output_file: os.PathLike, + *, + raster_device: str, + raster_dpi: Resolution, + pageno: int = 1, + page_dpi: Optional[Resolution] = None, + rotation: Optional[int] = None, + filter_vector: bool = False, +): + """Rasterize one page of a PDF at resolution raster_dpi in canvas units.""" + raster_dpi = raster_dpi.round(6) + if not page_dpi: + page_dpi = raster_dpi + + args_gs = ( + [ + GS, + '-dQUIET', + '-dSAFER', + '-dBATCH', + '-dNOPAUSE', + f'-sDEVICE={raster_device}', + f'-dFirstPage={pageno}', + f'-dLastPage={pageno}', + f'-r{raster_dpi.x:f}x{raster_dpi.y:f}', + ] + + (['-dFILTERVECTOR'] if filter_vector else []) + + [ + '-o', + '-', + '-sstdout=%stderr', + '-dAutoRotatePages=/None', # Probably has no effect on raster + '-f', + fspath(input_file), + ] + ) + + try: + p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True) + except CalledProcessError as e: + log.error(e.stderr.decode(errors='replace')) + raise SubprocessOutputError('Ghostscript rasterizing failed') + else: + stderr = p.stderr.decode(errors='replace') + if _gs_error_reported(stderr): + log.error(stderr) + + with Image.open(BytesIO(p.stdout)) as im: + if rotation is not None: + log.debug("Rotating output by %i", rotation) + # rotation is a clockwise angle and Image.ROTATE_* is + # counterclockwise so this cancels out the rotation + if rotation == 90: + im = im.transpose(Image.ROTATE_90) + elif rotation == 180: + im = im.transpose(Image.ROTATE_180) + elif rotation == 270: + im = im.transpose(Image.ROTATE_270) + if rotation % 180 == 90: + page_dpi = page_dpi.flip_axis() + im.save(fspath(output_file), dpi=page_dpi) + + +class GhostscriptFollower: + re_process = re.compile(r"Processing pages \d+ through (\d+).") + re_page = re.compile(r"Page (\d+)") + + def __init__(self, progressbar_class): + self.count = 0 + self.progressbar_class = progressbar_class + self.progressbar = None + + def __call__(self, line): + if not self.progressbar_class: + return + if not self.progressbar: + m = self.re_process.match(line.strip()) + if m: + self.count = int(m.group(1)) + self.progressbar = self.progressbar_class( + total=self.count, desc="PDF/A conversion", unit='page' + ) + return + else: + m = self.re_page.match(line.strip()) + if m: + self.progressbar.update() + + +def generate_pdfa( + pdf_pages, + output_file: os.PathLike, + *, + compression: str, + pdf_version: str = '1.5', + pdfa_part: str = '2', + progressbar_class=None, +): + # Ghostscript's compression is all or nothing. We can either force all images + # to JPEG, force all to Flate/PNG, or let it decide how to encode the images. + # In most case it's best to let it decide. + compression_args = [] + if compression == 'jpeg': + compression_args = [ + "-dAutoFilterColorImages=false", + "-dColorImageFilter=/DCTEncode", + "-dAutoFilterGrayImages=false", + "-dGrayImageFilter=/DCTEncode", + ] + elif compression == 'lossless': + compression_args = [ + "-dAutoFilterColorImages=false", + "-dColorImageFilter=/FlateEncode", + "-dAutoFilterGrayImages=false", + "-dGrayImageFilter=/FlateEncode", + ] + else: + compression_args = [ + "-dAutoFilterColorImages=true", + "-dAutoFilterGrayImages=true", + ] + + strategy = 'LeaveColorUnchanged' + # Older versions of Ghostscript expect a leading slash in + # sColorConversionStrategy, newer ones should not have it. See Ghostscript + # git commit fe1c025d. + strategy = ('/' + strategy) if version() < '9.19' else strategy + + if version() == '9.23': + # 9.23: added JPEG passthrough as a new feature, but with a bug that + # incorrectly formats some images. Fixed as of 9.24. So we disable this + # feature for 9.23. + # https://bugs.ghostscript.com/show_bug.cgi?id=699216 + compression_args.append('-dPassThroughJPEGImages=false') + + # nb no need to specify ProcessColorModel when ColorConversionStrategy + # is set; see: + # https://bugs.ghostscript.com/show_bug.cgi?id=699392 + args_gs = ( + [ + GS, + "-dBATCH", + "-dNOPAUSE", + "-dSAFER", + "-dCompatibilityLevel=" + str(pdf_version), + "-sDEVICE=pdfwrite", + "-dAutoRotatePages=/None", + "-sColorConversionStrategy=" + strategy, + ] + + compression_args + + [ + "-dJPEGQ=95", + "-dPDFA=" + pdfa_part, + "-dPDFACompatibilityPolicy=1", + "-o", + "-", + "-sstdout=%stderr", + ] + ) + args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs + + try: + with Path(output_file).open('wb') as output: + p = run_polling_stderr( + args_gs, + stdout=output, + stderr=PIPE, + check=True, + text=True, + encoding='utf-8', + errors='replace', + callback=GhostscriptFollower(progressbar_class), + ) + except CalledProcessError as e: + # Ghostscript does not change return code when it fails to create + # PDF/A - check PDF/A status elsewhere + log.error(e.stderr) + raise SubprocessOutputError('Ghostscript PDF/A rendering failed') from e + else: + stderr = p.stderr + # If there is an error we log the whole stderr, except for filtering + # duplicates. + if _gs_error_reported(stderr): + last_part = None + repcount = 0 + for part in stderr.split('****'): + if part != last_part: + if repcount > 1: + log.error(f"(previous error message repeated {repcount} times)") + repcount = 0 + log.error(part) + else: + repcount += 1 + last_part = part diff --git a/src/ocrmypdf/exec/jbig2enc.py b/src/ocrmypdf/_exec/jbig2enc.py similarity index 51% rename from src/ocrmypdf/exec/jbig2enc.py rename to src/ocrmypdf/_exec/jbig2enc.py index 771e58a3..2e8a058b 100644 --- a/src/ocrmypdf/exec/jbig2enc.py +++ b/src/ocrmypdf/_exec/jbig2enc.py @@ -1,28 +1,18 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -from functools import lru_cache -from subprocess import PIPE, run - -from . import get_version -from ..exceptions import MissingDependencyError +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Interface to jbig2 executable""" + +from subprocess import PIPE + +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.subprocess import get_version, run -@lru_cache(maxsize=1) def version(): return get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*') @@ -51,9 +41,17 @@ def convert_group(*, cwd, infiles, out_prefix): return proc +def convert_group_mp(args): + return convert_group(cwd=args[0], infiles=args[1], out_prefix=args[2]) + + def convert_single(*, cwd, infile, outfile): args = ['jbig2', '-p', infile] with open(outfile, 'wb') as fstdout: proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE) proc.check_returncode() return proc + + +def convert_single_mp(args): + return convert_single(cwd=args[0], infile=args[1], outfile=args[2]) diff --git a/src/ocrmypdf/_exec/pngquant.py b/src/ocrmypdf/_exec/pngquant.py new file mode 100644 index 00000000..ca8a4542 --- /dev/null +++ b/src/ocrmypdf/_exec/pngquant.py @@ -0,0 +1,65 @@ +# © 2018 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Interface to pngquant executable""" + +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from subprocess import PIPE + +from PIL import Image + +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.subprocess import get_version, run + + +def version(): + return get_version('pngquant', regex=r'(\d+(\.\d+)*).*') + + +def available(): + try: + version() + except MissingDependencyError: + return False + return True + + +@contextmanager +def input_as_png(input_file: Path): + if not input_file.name.endswith('.png'): + with Image.open(input_file) as im: + bio = BytesIO() + im.save(bio, format='png') + bio.seek(0) + yield bio + else: + with open(input_file, 'rb') as f: + yield f + + +def quantize(input_file: Path, output_file: Path, quality_min: int, quality_max: int): + with input_as_png(input_file) as input_stream: + args = [ + 'pngquant', + '--force', + '--skip-if-larger', + '--quality', + f'{quality_min}-{quality_max}', + '--', # pngquant: stop processing arguments + '-', # pngquant: stream input and output + ] + result = run(args, stdin=input_stream, stdout=PIPE, stderr=PIPE, check=False) + + if result.returncode == 0: + # input_file could be the same as output_file, so we defer the write + output_file.write_bytes(result.stdout) + + +def quantize_mp(args): + return quantize(*args) diff --git a/src/ocrmypdf/_exec/tesseract.py b/src/ocrmypdf/_exec/tesseract.py new file mode 100644 index 00000000..119771ec --- /dev/null +++ b/src/ocrmypdf/_exec/tesseract.py @@ -0,0 +1,342 @@ +# © 2017 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Interface to Tesseract executable""" + +import logging +import os +import re +import shutil +from collections import namedtuple +from distutils.version import StrictVersion +from os import fspath +from pathlib import Path +from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired +from typing import List, Optional + +from PIL import Image + +from ocrmypdf.exceptions import ( + MissingDependencyError, + SubprocessOutputError, + TesseractConfigError, +) +from ocrmypdf.subprocess import get_version, run + +log = logging.getLogger(__name__) + +OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence')) + +HOCR_TEMPLATE = """ + + + + + + + + + +
+
+ + +""" + + +class TesseractLoggerAdapter(logging.LoggerAdapter): + def process(self, msg, kwargs): + kwargs['extra'] = self.extra + return '[tesseract] %s' % (msg), kwargs + + +class TesseractVersion(StrictVersion): + version_re = re.compile( + r''' + ^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch + [-]? # optional hyphen separator + (?:(alpha|beta|rc|dev)[.\-\ ]?(\d+)?)? # 5/prerelease, 6/prerelease_num + (?:-(\d+)-g[0-9a-f]+)? # untagged git version + $ + ''', + re.VERBOSE | re.ASCII, + ) + + def parse(self, vstring): + try: + super().parse(vstring) + except TypeError as e: + if 'int() argument must be a string' in str(e): + super().parse(vstring + '0') + + +def version(): + return get_version('tesseract', regex=r'tesseract\s(.+)') + + +def has_user_words(): + """Does Tesseract have --user-words capability? + + Not available in 4.0, but available in 4.1. Also available in 3.x, but + we no longer support 3.x. + """ + return version() >= '4.1' + + +def get_languages(): + def lang_error(output): + msg = ( + "Tesseract failed to report available languages.\n" + "Output from Tesseract:\n" + "-----------\n" + ) + msg += output + return msg + + args_tess = ['tesseract', '--list-langs'] + try: + proc = run( + args_tess, + text=True, + stdout=PIPE, + stderr=STDOUT, + logs_errors_to_stdout=True, + check=True, + ) + output = proc.stdout + except CalledProcessError as e: + raise MissingDependencyError(lang_error(e.output)) from e + + for line in output.splitlines(): + if line.startswith('Error'): + raise MissingDependencyError(lang_error(output)) + _header, *rest = output.splitlines() + return set(lang.strip() for lang in rest) + + +def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]: + args = ['tesseract'] + if langs: + args.extend(['-l', '+'.join(langs)]) + if engine_mode is not None: + args.extend(['--oem', str(engine_mode)]) + return args + + +def get_orientation(input_file: Path, engine_mode: Optional[int], timeout: float): + args_tesseract = tess_base_args(['osd'], engine_mode) + [ + '--psm', + '0', + fspath(input_file), + 'stdout', + ] + + try: + p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True) + stdout = p.stdout + except TimeoutExpired: + return OrientationConfidence(angle=0, confidence=0.0) + except CalledProcessError as e: + tesseract_log_output(e.stdout) + tesseract_log_output(e.stderr) + if ( + b'Too few characters. Skipping this page' in e.output + or b'Image too large' in e.output + ): + return OrientationConfidence(0, 0) + raise SubprocessOutputError() from e + else: + osd = {} + for line in stdout.decode().splitlines(): + line = line.strip() + parts = line.split(':', maxsplit=2) + if len(parts) == 2: + osd[parts[0].strip()] = parts[1].strip() + + angle = int(osd.get('Orientation in degrees', 0)) + oc = OrientationConfidence( + angle=angle, confidence=float(osd.get('Orientation confidence', 0)) + ) + return oc + + +def tesseract_log_output(stream): + tlog = TesseractLoggerAdapter( + log, extra=log.extra if hasattr(log, 'extra') else None + ) + + if not stream: + return + try: + text = stream.decode() + except UnicodeDecodeError: + text = stream.decode('utf-8', 'ignore') + + lines = text.splitlines() + for line in lines: + if line.startswith("Tesseract Open Source"): + continue + elif line.startswith("Warning in pixReadMem"): + continue + elif 'diacritics' in line: + tlog.warning("lots of diacritics - possibly poor OCR") + elif line.startswith('OSD: Weak margin'): + tlog.warning("unsure about page orientation") + elif 'Error in pixScanForForeground' in line: + pass # Appears to be spurious/problem with nonwhite borders + elif 'Error in boxClipToRectangle' in line: + pass # Always appears with pixScanForForeground message + elif 'parameter not found: ' in line.lower(): + tlog.error(line.strip()) + problem = line.split('found: ')[1] + raise TesseractConfigError(problem) + elif 'error' in line.lower() or 'exception' in line.lower(): + tlog.error(line.strip()) + elif 'warning' in line.lower(): + tlog.warning(line.strip()) + elif 'read_params_file' in line.lower(): + tlog.error(line.strip()) + else: + tlog.info(line.strip()) + + +def page_timedout(timeout): + if timeout == 0: + return + log.warning("[tesseract] took too long to OCR - skipping") + + +def _generate_null_hocr(output_hocr, output_text, image): + """Produce a .hocr file that reports no text detected on a page that is + the same size as the input image.""" + with Image.open(image) as im: + w, h = im.size + + output_hocr.write_text(HOCR_TEMPLATE.format(w, h), encoding='utf-8') + output_text.write_text('[skipped page]', encoding='utf-8') + + +def generate_hocr( + *, + input_file: Path, + output_hocr: Path, + output_text: Path, + languages: List[str], + engine_mode: int, + tessconfig: List[str], + timeout: float, + pagesegmode: int, + user_words, + user_patterns, +): + prefix = output_hocr.with_suffix('') + + args_tesseract = tess_base_args(languages, engine_mode) + + if pagesegmode is not None: + args_tesseract.extend(['--psm', str(pagesegmode)]) + + if user_words: + args_tesseract.extend(['--user-words', user_words]) + + if user_patterns: + args_tesseract.extend(['--user-patterns', user_patterns]) + + # Reminder: test suite tesseract test plugins will break after any changes + # to the number of order parameters here + args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig) + try: + p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True) + stdout = p.stdout + except TimeoutExpired: + # Generate a HOCR file with no recognized text if tesseract times out + # Temporary workaround to hocrTransform not being able to function if + # it does not have a valid hOCR file. + page_timedout(timeout) + _generate_null_hocr(output_hocr, output_text, input_file) + except CalledProcessError as e: + tesseract_log_output(e.output) + if b'Image too large' in e.output: + _generate_null_hocr(output_hocr, output_text, input_file) + return + + raise SubprocessOutputError() from e + else: + tesseract_log_output(stdout) + # The sidecar text file will get the suffix .txt; rename it to + # whatever caller wants it named + if prefix.with_suffix('.txt').exists(): + shutil.move(prefix.with_suffix('.txt'), output_text) + + +def use_skip_page(output_pdf, output_text): + output_text.write_text('[skipped page]', encoding='utf-8') + + # A 0 byte file to the output to indicate a skip + output_pdf.write_bytes(b'') + + +def generate_pdf( + *, + input_file: Path, + output_pdf: Path, + output_text: Path, + languages: List[str], + engine_mode: int, + tessconfig: List[str], + timeout: float, + pagesegmode: int, + user_words, + user_patterns, +): + """Use Tesseract to render a PDF. + + input_file -- image to analyze + output_pdf -- file to generate + output_text -- OCR text file + languages -- list of languages to consider + engine_mode -- engine mode argument for tess v4 + tessconfig -- tesseract configuration + timeout -- timeout (seconds) + """ + + args_tesseract = tess_base_args(languages, engine_mode) + + if pagesegmode is not None: + args_tesseract.extend(['--psm', str(pagesegmode)]) + + args_tesseract.extend(['-c', 'textonly_pdf=1']) + + if user_words: + args_tesseract.extend(['--user-words', user_words]) + + if user_patterns: + args_tesseract.extend(['--user-patterns', user_patterns]) + + prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes + + # Reminder: test suite tesseract test plugins might break after any changes + # to the number of order parameters here + + args_tesseract.extend([input_file, prefix, 'pdf', 'txt'] + tessconfig) + try: + p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True) + stdout = p.stdout + if os.path.exists(prefix + '.txt'): + shutil.move(prefix + '.txt', output_text) + except TimeoutExpired: + page_timedout(timeout) + use_skip_page(output_pdf, output_text) + except CalledProcessError as e: + tesseract_log_output(e.output) + if b'Image too large' in e.output: + use_skip_page(output_pdf, output_text) + return + raise SubprocessOutputError() from e + else: + tesseract_log_output(stdout) diff --git a/src/ocrmypdf/_exec/unpaper.py b/src/ocrmypdf/_exec/unpaper.py new file mode 100644 index 00000000..5226326d --- /dev/null +++ b/src/ocrmypdf/_exec/unpaper.py @@ -0,0 +1,134 @@ +# © 2015 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# unpaper documentation: +# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md + +"""Interface to unpaper executable""" + +import logging +import os +import shlex +from decimal import Decimal +from pathlib import Path +from subprocess import PIPE, STDOUT +from tempfile import TemporaryDirectory +from typing import List, Optional, Tuple, Union + +from PIL import Image + +from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError +from ocrmypdf.subprocess import get_version +from ocrmypdf.subprocess import run as external_run + +DecFloat = Union[Decimal, float] + +log = logging.getLogger(__name__) + + +def version() -> str: + return get_version('unpaper') + + +def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]: + SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'} + with Image.open(input_file) as im: + im_modified = False + if im.mode not in SUFFIXES: + log.info("Converting image to other colorspace") + try: + if im.mode == 'P' and len(im.getcolors()) == 2: + im = im.convert(mode='1') + else: + im = im.convert(mode='RGB') + except IOError as e: + raise MissingDependencyError( + "Could not convert image with type " + im.mode + ) from e + else: + im_modified = True + try: + suffix = SUFFIXES[im.mode] + except KeyError: + raise MissingDependencyError( + "Failed to convert image to a supported format." + ) from None + + if im_modified or input_file.suffix != '.pnm': + input_pnm = tmpdir / 'input.pnm' + im.save(input_pnm, format='PPM') + else: + # No changes, PNG input, just use the file we already have + input_pnm = input_file + output_pnm = tmpdir / f'output{suffix}' + return input_pnm, output_pnm + + +def run( + input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: List[str] +) -> None: + args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args + + with TemporaryDirectory() as tmpdir: + input_pnm, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file) + + # To prevent any shenanigans from accepting arbitrary parameters in + # --unpaper-args, we: + # 1) run with cwd set to a tmpdir with only unpaper's files + # 2) forbid the use of '/' in arguments, to prevent changing paths + # 3) append absolute paths for the input and output file + # This should ensure that a user cannot clobber some other file with + # their unpaper arguments (whether intentionally or otherwise) + args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)]) + external_run( + args_unpaper, + close_fds=True, + check=True, + stderr=STDOUT, # unpaper writes logging output to stdout and stderr + stdout=PIPE, # and cannot send file output to stdout + cwd=tmpdir, + logs_errors_to_stdout=True, + ) + try: + with Image.open(output_pnm) as imout: + imout.save(output_file, dpi=(dpi, dpi)) + except (FileNotFoundError, OSError): + raise SubprocessOutputError( + "unpaper: failed to produce the expected output file. " + + " Called with: " + + str(args_unpaper) + ) from None + + +def validate_custom_args(args: str) -> List[str]: + unpaper_args = shlex.split(args) + if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args): + raise ValueError('No filenames allowed in --unpaper-args') + return unpaper_args + + +def clean( + input_file: Path, + output_file: Path, + *, + dpi: DecFloat, + unpaper_args: Optional[List[str]] = None, +): + default_args = [ + '--layout', + 'none', + '--mask-scan-size', + '100', # don't blank out narrow columns + '--no-border-align', # don't align visible content to borders + '--no-mask-center', # don't center visible content within page + '--no-grayfilter', # don't remove light gray areas + '--no-blackfilter', # don't remove solid black areas + '--no-deskew', # don't deskew + ] + if not unpaper_args: + unpaper_args = default_args + run(input_file, output_file, dpi=dpi, mode_args=unpaper_args) diff --git a/src/ocrmypdf/_graft.py b/src/ocrmypdf/_graft.py new file mode 100644 index 00000000..ba4b050f --- /dev/null +++ b/src/ocrmypdf/_graft.py @@ -0,0 +1,314 @@ +# © 2018 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import uuid +from contextlib import suppress +from pathlib import Path +from typing import Optional + +import pikepdf +from pikepdf.objects import Dictionary, Name + +log = logging.getLogger(__name__) +MAX_REPLACE_PAGES = 100 + + +def _ensure_dictionary(obj, name): + if name not in obj: + obj[name] = Dictionary({}) + return obj[name] + + +def _update_resources(*, obj, font, font_key, procset): + """Update this obj's fonts with a reference to the Glyphless font. + + obj can be a page or Form XObject. + """ + + resources = _ensure_dictionary(obj, Name.Resources) + fonts = _ensure_dictionary(resources, Name.Font) + if font_key is not None and font_key not in fonts: + fonts[font_key] = font + + # Reassign /ProcSet to one that just lists everything - ProcSet is + # obsolete and doesn't matter but recommended for old viewer support + if procset: + resources['/ProcSet'] = procset + + +def strip_invisible_text(pdf, page): + stream = [] + in_text_obj = False + render_mode = 0 + text_objects = [] + + page.page_contents_coalesce() + for operands, operator in pikepdf.parse_content_stream(page, ''): + if not in_text_obj: + if operator == pikepdf.Operator('BT'): + in_text_obj = True + render_mode = 0 + text_objects.append((operands, operator)) + else: + stream.append((operands, operator)) + else: + if operator == pikepdf.Operator('Tr'): + render_mode = operands[0] + text_objects.append((operands, operator)) + if operator == pikepdf.Operator('ET'): + in_text_obj = False + if render_mode != 3: + stream.extend(text_objects) + text_objects.clear() + + def convert(op): + try: + return op.unparse() + except AttributeError: + return str(op).encode('ascii') + + lines = [] + + for operands, operator in stream: + if operator == pikepdf.Operator('INLINE IMAGE'): + iim = operands[0] + line = iim.unparse() + else: + line = b' '.join(convert(op) for op in operands) + b' ' + operator.unparse() + lines.append(line) + + content_stream = b'\n'.join(lines) + page.Contents = pikepdf.Stream(pdf, content_stream) + + +class OcrGrafter: + def __init__(self, context): + self.context = context + self.path_base = context.origin + + self.pdf_base = pikepdf.open(self.path_base) + self.font, self.font_key = None, None + + self.pdfinfo = context.pdfinfo + self.output_file = context.get_path('graft_layers.pdf') + + self.procset = self.pdf_base.make_indirect( + pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]') + ) + + self.emplacements = 1 + self.interim_count = 0 + + def graft_page( + self, + *, + pageno: int, + image: Optional[Path], + textpdf: Optional[Path], + autorotate_correction: int, + ): + if textpdf and not self.font: + self.font, self.font_key = self._find_font(textpdf) + + emplaced_page = False + content_rotation = self.pdfinfo[pageno].rotation + path_image = Path(image).resolve() if image else None + if path_image is not None and path_image != self.path_base: + # We are updating the old page with a rasterized PDF of the new + # page (without changing objgen, to preserve references) + log.debug("Emplacement update") + with pikepdf.open(image) as pdf_image: + self.emplacements += 1 + foreign_image_page = pdf_image.pages[0] + self.pdf_base.pages.append(foreign_image_page) + local_image_page = self.pdf_base.pages[-1] + self.pdf_base.pages[pageno].emplace(local_image_page) + del self.pdf_base.pages[-1] + emplaced_page = True + + # Calculate if the text is misaligned compared to the content + if emplaced_page: + content_rotation = autorotate_correction + text_rotation = autorotate_correction + text_misaligned = (text_rotation - content_rotation) % 360 + log.debug( + f"Text rotation: (text, autorotate, content) -> text misalignment = " + f"({text_rotation}, {autorotate_correction}, {content_rotation}) -> {text_misaligned}" + ) + + if textpdf and self.font: + # Graft the text layer onto this page, whether new or old, possibly + # rotating the text layer by the amount is misaligned. + strip_old = self.context.options.redo_ocr + self._graft_text_layer( + page_num=pageno + 1, + textpdf=textpdf, + font=self.font, + font_key=self.font_key, + text_rotation=text_misaligned, + procset=self.procset, + strip_old_text=strip_old, + ) + + # Correct the overall page rotation if needed, now that the text and content + # are aligned + page_rotation = (content_rotation - autorotate_correction) % 360 + self.pdf_base.pages[pageno].Rotate = page_rotation + log.debug( + f"Page rotation: (content, auto) -> page = " + f"({content_rotation}, {autorotate_correction}) -> {page_rotation}" + ) + if self.emplacements % MAX_REPLACE_PAGES == 0: + self.save_and_reload() + + def save_and_reload(self): + """Save and reload the Pdf. + + This will keep a lid on our memory usage for very large files. Attach + the font to page 1 even if page 1 doesn't use it, so we have a way to get it + back. + """ + + page0 = self.pdf_base.pages[0] + _update_resources( + obj=page0, font=self.font, font_key=self.font_key, procset=self.procset + ) + + # We cannot read and write the same file, that will corrupt it + # but we don't to keep more copies than we need to. Delete intermediates. + # {interim_count} is the opened file we were updating + # {interim_count - 1} can be deleted + # {interim_count + 1} is the new file will produce and open + old_file = self.output_file.with_suffix(f'.working{self.interim_count - 1}.pdf') + if not self.context.options.keep_temporary_files: + with suppress(FileNotFoundError): + old_file.unlink() + + next_file = self.output_file.with_suffix( + f'.working{self.interim_count + 1}.pdf' + ) + self.pdf_base.save(next_file) + self.pdf_base.close() + + self.pdf_base = pikepdf.open(next_file) + self.procset = self.pdf_base.pages[0].Resources.ProcSet + self.font, self.font_key = None, None # Ensure we reacquire this information + self.interim_count += 1 + + def finalize(self): + self.pdf_base.save(self.output_file) + self.pdf_base.close() + return self.output_file + + def _find_font(self, text): + """Copy a font from the filename text into pdf_base""" + + font, font_key = None, None + possible_font_names = ('/f-0-0', '/F1') + try: + with pikepdf.open(text) as pdf_text: + try: + pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {}) + except (AttributeError, IndexError, KeyError): + return None, None + pdf_text_font = None + for f in possible_font_names: + pdf_text_font = pdf_text_fonts.get(f, None) + if pdf_text_font is not None: + font_key = f + break + if pdf_text_font: + font = self.pdf_base.copy_foreign(pdf_text_font) + return font, font_key + except (FileNotFoundError, pikepdf.PdfError): + # PdfError occurs if a 0-length file is written e.g. due to OCR timeout + return None, None + + def _graft_text_layer( + self, + *, + page_num: int, + textpdf: Path, + font: pikepdf.Object, + font_key: pikepdf.Object, + procset: pikepdf.Object, + text_rotation: int, + strip_old_text: bool, + ): + """Insert the text layer from text page 0 on to pdf_base at page_num""" + + log.debug("Grafting") + if Path(textpdf).stat().st_size == 0: + return + + # This is a pointer indicating a specific page in the base file + with pikepdf.open(textpdf) as pdf_text: + pdf_text_contents = pdf_text.pages[0].Contents.read_bytes() + + base_page = self.pdf_base.pages.p(page_num) + + # The text page always will be oriented up by this stage but the original + # content may have a rotation applied. Wrap the text stream with a rotation + # so it will be oriented the same way as the rest of the page content. + # (Previous versions OCRmyPDF rotated the content layer to match the text.) + mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)] + wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1] + + mediabox = [float(base_page.MediaBox[v]) for v in range(4)] + wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1] + + translate = pikepdf.PdfMatrix().translated(-wt / 2, -ht / 2) + untranslate = pikepdf.PdfMatrix().translated(wp / 2, hp / 2) + corner = pikepdf.PdfMatrix().translated(mediabox[0], mediabox[1]) + # -rotation because the input is a clockwise angle and this formula + # uses CCW + text_rotation = -text_rotation % 360 + rotate = pikepdf.PdfMatrix().rotated(text_rotation) + + # Because of rounding of DPI, we might get a text layer that is not + # identically sized to the target page. Scale to adjust. Normally this + # is within 0.998. + if text_rotation in (90, 270): + wt, ht = ht, wt + scale_x = wp / wt + scale_y = hp / ht + + # log.debug('%r', scale_x, scale_y) + scale = pikepdf.PdfMatrix().scaled(scale_x, scale_y) + + # Translate the text so it is centered at (0, 0), rotate it there, adjust + # for a size different between initial and text PDF, then untranslate, and + # finally move the lower left corner to match the mediabox + ctm = translate @ rotate @ scale @ untranslate @ corner + + base_resources = _ensure_dictionary(base_page, Name.Resources) + base_xobjs = _ensure_dictionary(base_resources, Name.XObject) + text_xobj_name = Name('/' + str(uuid.uuid4())) + xobj = self.pdf_base.make_stream(pdf_text_contents) + base_xobjs[text_xobj_name] = xobj + xobj.Type = Name.XObject + xobj.Subtype = Name.Form + xobj.FormType = 1 + xobj.BBox = mediabox + _update_resources( + obj=xobj, font=font, font_key=font_key, procset=[Name.PDF] + ) + + pdf_draw_xobj = ( + (b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n' + ) + new_text_layer = pikepdf.Stream(self.pdf_base, pdf_draw_xobj) + + if strip_old_text: + strip_invisible_text(self.pdf_base, base_page) + + base_page.page_contents_add(new_text_layer, prepend=True) + + _update_resources( + obj=base_page, font=font, font_key=font_key, procset=procset + ) diff --git a/src/ocrmypdf/_jobcontext.py b/src/ocrmypdf/_jobcontext.py index c9367ba6..2363ccbb 100644 --- a/src/ocrmypdf/_jobcontext.py +++ b/src/ocrmypdf/_jobcontext.py @@ -1,83 +1,103 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os import shutil import sys -from contextlib import suppress -from multiprocessing.managers import SyncManager +from argparse import Namespace +from copy import copy +from pathlib import Path +from typing import Iterator -from .pdfinfo import PdfInfo +from pluggy import PluginManager + +from ocrmypdf.pdfinfo import PdfInfo +from ocrmypdf.pdfinfo.info import PageInfo -class JobContext: - """Holds our context for a particular run of the pipeline +class PdfContext: + """Holds the context for a particular run of the pipeline.""" - A multiprocessing manager effectively creates a separate process - that keeps the master job context object. Other threads access - job context via multiprocessing proxy objects. + options: Namespace #: The specified options for processing this PDF. + origin: Path #: The filename of the original input file. + pdfinfo: PdfInfo #: Detailed data for this PDF. + plugin_manager: PluginManager #: PluginManager for processing the current PDF. - While this would naturally lend itself @property's it seems to make - a little more sense to use functions to make it explicitly that the - invocation requires marshalling data across a process boundary. + def __init__( + self, + options: Namespace, + work_folder: Path, + origin: Path, + pdfinfo: PdfInfo, + plugin_manager, + ): + self.options = options + self.work_folder = work_folder + self.origin = origin + self.pdfinfo = pdfinfo + self.plugin_manager = plugin_manager + def get_path(self, name: str) -> Path: + """Generate a ``Path`` for an intermediate file involved in processing. + + The path will be in a temporary folder that is common for all processing + of this particular PDF. + """ + return self.work_folder / name + + def get_page_contexts(self) -> Iterator['PageContext']: + """Get all ``PageContext`` for this PDF.""" + npages = len(self.pdfinfo) + for n in range(npages): + yield PageContext(self, n) + + +class PageContext: + """Holds our context for a page. + + Must be pickable, so stores only intrinsic/simple data elements or those + capable of their serializing themselves via ``__getstate__``. """ - def __init__(self): - self.pdfinfo = None - self.options = None - self.work_folder = None - self.rotations = {} + options: Namespace #: The specified options for processing this PDF. + origin: Path #: The filename of the original input file. + pageno: int #: This page number (zero-based). + pageinfo: PageInfo #: Information on this page. + plugin_manager: PluginManager #: PluginManager for processing the current PDF. - def generate_pdfinfo(self, infile): - self.pdfinfo = PdfInfo(infile) + def __init__(self, pdf_context: PdfContext, pageno): + self.work_folder = pdf_context.work_folder + self.origin = pdf_context.origin + self.options = pdf_context.options + self.pageno = pageno + self.pageinfo = pdf_context.pdfinfo[pageno] + self.plugin_manager = pdf_context.plugin_manager - def get_pdfinfo(self): - "What we know about the input PDF" - return self.pdfinfo + def get_path(self, name: str) -> Path: + """Generate a ``Path`` for a file that is part of processing this page. - def set_pdfinfo(self, pdfinfo): - self.pdfinfo = pdfinfo + The path will be based in a common temporary folder and have a prefix based + on the page number. + """ + return self.work_folder / ("%06d_%s" % (self.pageno + 1, name)) - def get_options(self): - return self.options + def __getstate__(self): + state = self.__dict__.copy() - def set_options(self, options): - self.options = options - - def get_work_folder(self): - return self.work_folder - - def set_work_folder(self, work_folder): - self.work_folder = work_folder - - def get_rotation(self, pageno): - return self.rotations.get(pageno, 0) - - def set_rotation(self, pageno, value): - self.rotations[pageno] = value + state['options'] = copy(self.options) + if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)): + state['options'].input_file = 'stream' + if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)): + state['options'].output_file = 'stream' + return state -class JobContextManager(SyncManager): - pass - - -def cleanup_working_files(work_folder, options): +def cleanup_working_files(work_folder: Path, options: Namespace): if options.keep_temporary_files: - print(f"Temporary working files saved at:\n{work_folder}", file=sys.stderr) + print(f"Temporary working files retained at:\n{work_folder}", file=sys.stderr) else: - with suppress(FileNotFoundError): - shutil.rmtree(work_folder) + shutil.rmtree(work_folder, ignore_errors=True) diff --git a/src/ocrmypdf/_logging.py b/src/ocrmypdf/_logging.py new file mode 100644 index 00000000..e33616a4 --- /dev/null +++ b/src/ocrmypdf/_logging.py @@ -0,0 +1,50 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import sys +from contextlib import suppress + +from tqdm import tqdm + + +class PageNumberFilter(logging.Filter): + def filter(self, record): + pageno = getattr(record, 'pageno', None) + if isinstance(pageno, int): + record.pageno = f'{pageno:5d} ' + elif pageno is None: + record.pageno = '' + return True + + +class TqdmConsole: + """Wrapper to log messages in a way that is compatible with tqdm progress bar + + This routes log messages through tqdm so that it can print them above the + progress bar, and then refresh the progress bar, rather than overwriting + it which looks messy. + + For some reason Python 3.6 prints extra empty messages from time to time, + so we suppress those. + """ + + def __init__(self, file): + self.file = file + self.py36 = sys.version_info[0:2] == (3, 6) + + def write(self, msg): + # When no progress bar is active, tqdm.write() routes to print() + if self.py36: + if msg.strip() != '': + tqdm.write(msg.rstrip(), end='\n', file=self.file) + else: + tqdm.write(msg.rstrip(), end='\n', file=self.file) + + def flush(self): + with suppress(AttributeError): + self.file.flush() diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index ab11da6e..ce3b8d88 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -1,81 +1,64 @@ # © 2016 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging import os import re import sys from contextlib import suppress from datetime import datetime, timezone from pathlib import Path -from shutil import copyfile, copyfileobj +from shutil import copyfileobj +from typing import Dict, Iterable, Optional import img2pdf -from PIL import Image -from ruffus import Pipeline, formatter, regex, suffix - import pikepdf from pikepdf.models.metadata import encode_pdf_date +from PIL import Image, ImageColor, ImageDraw -from . import PROGRAM_NAME, VERSION, leptonica -from ._weave import weave_layers -from .exceptions import ( +from ocrmypdf import leptonica +from ocrmypdf._concurrent import Executor +from ocrmypdf._exec import unpaper +from ocrmypdf._jobcontext import PageContext, PdfContext +from ocrmypdf._version import PROGRAM_NAME +from ocrmypdf._version import __version__ as VERSION +from ocrmypdf.exceptions import ( DpiError, EncryptedPdfError, InputFileError, PriorOcrFoundError, UnsupportedImageFormatError, ) -from .exec import ghostscript, tesseract -from .helpers import flatten_groups, is_iterable_notstr, page_number, re_symlink -from .hocrtransform import HocrTransform -from .optimize import optimize -from .pdfa import generate_pdfa_ps -from .pdfinfo import Colorspace, PdfInfo +from ocrmypdf.helpers import Resolution, safe_symlink +from ocrmypdf.hocrtransform import HocrTransform +from ocrmypdf.optimize import optimize +from ocrmypdf.pdfa import generate_pdfa_ps +from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo + +log = logging.getLogger(__name__) VECTOR_PAGE_DPI = 400 -# -# The Pipeline -# - -def triage_image_file(input_file, output_file, log, options): +def triage_image_file(input_file, output_file, options): + log.info("Input file is not a PDF, checking if it is an image...") try: - log.info("Input file is not a PDF, checking if it is an image...") im = Image.open(input_file) except EnvironmentError as e: - msg = str(e) - # Recover the original filename - realpath = '' - if os.path.islink(input_file): - realpath = os.path.realpath(input_file) - elif os.path.isfile(input_file): - realpath = '' - msg = msg.replace(input_file, realpath) - log.error(msg) + log.error(str(e).replace(str(input_file), str(options.input_file))) raise UnsupportedImageFormatError() from e - else: - log.info("Input file is an image") + with im: + log.info("Input file is an image") if 'dpi' in im.info: if im.info['dpi'] <= (96, 96) and not options.image_dpi: - log.info("Image size: (%d, %d)" % im.size) - log.info("Image resolution: (%d, %d)" % im.info['dpi']) + log.info("Image size: (%d, %d)", *im.size) + log.info("Image resolution: (%d, %d)", *im.info['dpi']) log.error( "Input file is an image, but the resolution (DPI) is " "not credible. Estimate the resolution at which the " @@ -83,7 +66,7 @@ def triage_image_file(input_file, output_file, log, options): ) raise DpiError() elif not options.image_dpi: - log.info("Image size: (%d, %d)" % im.size) + log.info("Image size: (%d, %d)", *im.size) log.error( "Input file is an image, but has no resolution (DPI) " "in its metadata. Estimate the resolution at which " @@ -100,22 +83,24 @@ def triage_image_file(input_file, output_file, log, options): if 'iccprofile' not in im.info: 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': - log.info('Input CMYK image has no ICC profile, not usable') + log.error("Input CMYK image has no ICC profile, not usable") raise UnsupportedImageFormatError() - im.close() try: log.info("Image seems valid. Try converting to PDF...") layout_fun = img2pdf.default_layout_fun if options.image_dpi: layout_fun = img2pdf.get_fixed_dpi_layout_fun( - (options.image_dpi, options.image_dpi) + Resolution(options.image_dpi, options.image_dpi) ) with open(output_file, 'wb') as outf: img2pdf.convert( - input_file, layout_fun=layout_fun, with_pdfrw=False, outputstream=outf + os.fspath(input_file), + layout_fun=layout_fun, + with_pdfrw=False, + outputstream=outf, ) log.info("Successfully converted to PDF, processing...") except img2pdf.ImageOpenError as e: @@ -139,42 +124,53 @@ def _pdf_guess_version(input_file, search_window=1024): return '' -def triage(input_file, output_file, log, context): - - options = context.get_options() +def triage(original_filename, input_file, output_file, options): try: if _pdf_guess_version(input_file): if options.image_dpi: log.warning( - "Argument --image-dpi ignored because the " + "Argument --image-dpi is being ignored because the " "input file is a PDF, not an image." ) - re_symlink(input_file, output_file, log) - return + # Origin file is a pdf create a symlink with pdf extension + safe_symlink(input_file, output_file) + return output_file except EnvironmentError as e: - log.error(e) - raise InputFileError() from e + log.debug(f"Temporary file was at: {input_file}") + msg = str(e).replace(str(input_file), original_filename) + raise InputFileError(msg) from e - triage_image_file(input_file, output_file, log, options) + triage_image_file(input_file, output_file, options) + return output_file -def repair_and_parse_pdf(input_file, output_file, log, context): - options = context.get_options() - copyfile(input_file, output_file) - - detailed_page_analysis = False - if options.redo_ocr: - detailed_page_analysis = True - +def get_pdfinfo( + input_file, + *, + executor: Executor, + detailed_analysis=False, + progbar=False, + max_workers=None, + check_pages=None, +) -> PdfInfo: try: - pdfinfo = PdfInfo( - output_file, detailed_page_analysis=detailed_page_analysis, log=log + return PdfInfo( + input_file, + detailed_analysis=detailed_analysis, + progbar=progbar, + max_workers=max_workers, + check_pages=check_pages, + executor=executor, ) except pikepdf.PasswordError as e: - raise EncryptedPdfError() + raise EncryptedPdfError() from e except pikepdf.PdfError as e: - log.error(e) - raise InputFileError() + raise InputFileError() from e + + +def validate_pdfinfo_options(context: PdfContext): + pdfinfo = context.pdfinfo + options = context.options if pdfinfo.needs_rendering: log.error( @@ -182,7 +178,6 @@ def repair_and_parse_pdf(input_file, output_file, log, context): "Designer and can only be read by Adobe Acrobat or Adobe Reader." ) raise InputFileError() - if pdfinfo.has_userunit and options.output_type.startswith('pdfa'): log.error( "This input file uses a PDF feature that is not supported " @@ -192,17 +187,17 @@ def repair_and_parse_pdf(input_file, output_file, log, context): "output these files.) Use --output-type=pdf instead." ) raise InputFileError() - if pdfinfo.has_acroform: if options.redo_ocr: log.error( "This PDF has a user fillable form. --redo-ocr is not " "currently possible on such files." ) - raise PriorOcrFoundError() + raise InputFileError() else: log.warning( - "This PDF has a fillable form. Chances are it is a pure digital " + "This PDF has a fillable form. " + "Chances are it is a pure digital " "document that does not need OCR." ) if not options.force_ocr: @@ -211,88 +206,86 @@ def repair_and_parse_pdf(input_file, output_file, log, context): "form and all filled form fields. The output PDF will be " "'flattened' and will no longer be fillable." ) - - context.set_pdfinfo(pdfinfo) - log.debug(pdfinfo) + context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options) -def get_pageinfo(input_file, context): - "Get zero-based page info implied by filename, e.g. 000002.pdf -> 1" - pageno = page_number(input_file) - 1 - pageinfo = context.get_pdfinfo()[pageno] - return pageinfo +def _vector_page_dpi(pageinfo): + return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0.0 def get_page_dpi(pageinfo, options): "Get the DPI when nonsquare DPI is tolerable" xres = max( - pageinfo.xres or VECTOR_PAGE_DPI, - options.oversample or 0, - VECTOR_PAGE_DPI if pageinfo.has_vector else 0, + pageinfo.dpi.x or VECTOR_PAGE_DPI, + options.oversample or 0.0, + _vector_page_dpi(pageinfo), ) yres = max( - pageinfo.yres or VECTOR_PAGE_DPI, + pageinfo.dpi.y or VECTOR_PAGE_DPI, options.oversample or 0, - VECTOR_PAGE_DPI if pageinfo.has_vector else 0, + _vector_page_dpi(pageinfo), ) - return (float(xres), float(yres)) + return Resolution(float(xres), float(yres)) -def get_page_square_dpi(pageinfo, options): +def get_page_square_dpi(pageinfo, options) -> Resolution: "Get the DPI when we require xres == yres, scaled to physical units" - xres = pageinfo.xres or 0 - yres = pageinfo.yres or 0 - userunit = pageinfo.userunit or 1 - return float( + xres = pageinfo.dpi.x or 0.0 + yres = pageinfo.dpi.y or 0.0 + userunit = float(pageinfo.userunit) or 1.0 + units = float( max( (xres * userunit) or VECTOR_PAGE_DPI, (yres * userunit) or VECTOR_PAGE_DPI, - VECTOR_PAGE_DPI if pageinfo.has_vector else 0, - options.oversample or 0, + _vector_page_dpi(pageinfo), + options.oversample or 0.0, ) ) + return Resolution(units, units) -def get_canvas_square_dpi(pageinfo, options): +def get_canvas_square_dpi(pageinfo, options) -> Resolution: """Get the DPI when we require xres == yres, in Postscript units""" - return float( + units = float( max( - (pageinfo.xres) or VECTOR_PAGE_DPI, - (pageinfo.yres) or VECTOR_PAGE_DPI, - VECTOR_PAGE_DPI if pageinfo.has_vector else 0, - options.oversample or 0, + (pageinfo.dpi.x) or VECTOR_PAGE_DPI, + (pageinfo.dpi.y) or VECTOR_PAGE_DPI, + _vector_page_dpi(pageinfo), + options.oversample or 0.0, ) ) + return Resolution(units, units) -def is_ocr_required(pageinfo, log, options): - page = pageinfo.pageno + 1 +def is_ocr_required(page_context: PageContext): + pageinfo = page_context.pageinfo + options = page_context.options + ocr_required = True - if pageinfo.has_text: - prefix = f"{page:4d}: page already has text! - " - + if options.pages and pageinfo.pageno not in options.pages: + log.debug(f"skipped {pageinfo.pageno} as requested by --pages {options.pages}") + ocr_required = False + elif pageinfo.has_text: if not options.force_ocr and not (options.skip_text or options.redo_ocr): - log.error(prefix + "aborting (use --force-ocr to force OCR)") - raise PriorOcrFoundError() + raise PriorOcrFoundError( + "page already has text! - aborting (use --force-ocr to force OCR; " + " see also help for the arguments --skip-text and --redo-ocr" + ) elif options.force_ocr: - log.info(prefix + "rasterizing text and running OCR anyway") + log.info("page already has text! - rasterizing text and running OCR anyway") ocr_required = True elif options.redo_ocr: if pageinfo.has_corrupt_text: log.warning( - prefix - + ( - "some text on this page cannot be mapped to characters: " - "consider using --force-ocr instead", - ) + "some text on this page cannot be mapped to characters: " + "consider using --force-ocr instead" ) - raise PriorOcrFoundError() # Wrong error but will do for now else: - log.info(prefix + "redoing OCR") + log.info("redoing OCR") ocr_required = True elif options.skip_text: - log.info(prefix + "skipping all processing on this page") + log.info("skipping all processing on this page") ocr_required = False elif not pageinfo.images and not options.lossless_reconstruction: # We found a page with no images and no text. That means it may @@ -305,14 +298,14 @@ def is_ocr_required(pageinfo, log, options): if options.force_ocr and options.oversample: # The user really wants to reprocess this file log.info( - f"{page:4d}: page has no images - " + "page has no images - " f"rasterizing at {options.oversample} DPI because " "--force-ocr --oversample was specified" ) elif options.force_ocr: # Warn the user they might not want to do this log.warning( - f"{page:4d}: page has no images - " + "page has no images - " "all vector content will be " f"rasterized at {VECTOR_PAGE_DPI} DPI, losing some resolution and likely " "increasing file size. Use --oversample to adjust the " @@ -320,7 +313,7 @@ def is_ocr_required(pageinfo, log, options): ) else: log.info( - f"{page:4d}: page has no images - " + "page has no images - " "skipping all processing on this page to avoid losing detail. " "Use --force-ocr if you wish to perform OCR on pages that " "have vector content." @@ -332,163 +325,105 @@ def is_ocr_required(pageinfo, log, options): if pixel_count > (options.skip_big * 1_000_000): ocr_required = False log.warning( - f"{page:4d}: page too big, skipping OCR " + "page too big, skipping OCR " f"({(pixel_count / 1_000_000):.1f} MPixels > {options.skip_big:.1f} MPixels --skip-big)" ) return ocr_required -def marker_pages(input_files, output_files, log, context): - - options = context.get_options() - work_folder = context.get_work_folder() - - if is_iterable_notstr(input_files): - input_file = input_files[0] - else: - input_file = input_files - - for oo in output_files: - with suppress(FileNotFoundError): - os.unlink(oo) - - # If no files were repaired the input will be empty - if not input_file: - log.error(f"{options.input_file}: file not found or invalid argument") - raise InputFileError() - - pdfinfo = context.get_pdfinfo() - npages = len(pdfinfo) - - # Ruffus needs to see a file for any task it generates, so make very - # file a symlink back to the source. - for n in range(npages): - page = Path(work_folder) / f'{(n + 1):06d}.marker.pdf' - page.symlink_to(input_file) # pylint: disable=E1101 - - -def ocr_or_skip(input_files, output_files, log, context): - options = context.get_options() - work_folder = context.get_work_folder() - pdfinfo = context.get_pdfinfo() - - for input_file in input_files: - pageno = page_number(input_file) - 1 - pageinfo = pdfinfo[pageno] - alt_suffix = ( - '.ocr.page.pdf' - if is_ocr_required(pageinfo, log, options) - else '.skip.page.pdf' - ) - - re_symlink( - input_file, - os.path.join(work_folder, os.path.basename(input_file)[0:6] + alt_suffix), - log, - ) - - -def rasterize_preview(input_file, output_file, log, context): - pageinfo = get_pageinfo(input_file, context) - options = context.get_options() - canvas_dpi = get_canvas_square_dpi(pageinfo, options) - page_dpi = get_page_square_dpi(pageinfo, options) - - ghostscript.rasterize_pdf( - input_file, - output_file, - xres=canvas_dpi, - yres=canvas_dpi, +def rasterize_preview(input_file: Path, page_context: PageContext): + output_file = page_context.get_path('rasterize_preview.jpg') + canvas_dpi = get_canvas_square_dpi(page_context.pageinfo, page_context.options) + page_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options) + page_context.plugin_manager.hook.rasterize_pdf_page( + input_file=input_file, + output_file=output_file, raster_device='jpeggray', - log=log, - page_dpi=(page_dpi, page_dpi), - pageno=page_number(input_file), + raster_dpi=canvas_dpi, + pageno=page_context.pageinfo.pageno + 1, + page_dpi=page_dpi, + rotation=0, + filter_vector=False, ) + return output_file -def orient_page(infiles, output_file, log, context): +def describe_rotation(page_context: PageContext, orient_conf, correction: int): """ - Work out orientation correct for each page. + Describe the page rotation we are going to perform. + """ + direction = {0: '⇧', 90: '⇨', 180: '⇩', 270: '⇦'} + turns = {0: ' ', 90: '⬏', 180: '↻', 270: '⬑'} + + existing_rotation = page_context.pageinfo.rotation + action = '' + if orient_conf.confidence >= page_context.options.rotate_pages_threshold: + if correction != 0: + action = 'will rotate ' + turns[correction] + else: + action = 'rotation appears correct' + else: + if correction != 0: + action = 'confidence too low to rotate' + else: + action = 'no change' + + facing = '' + + if existing_rotation != 0: + facing = f"with existing rotation {direction.get(existing_rotation, '?')}, " + facing += f"page is facing {direction.get(orient_conf.angle, '?')}" + + return f"{facing}, confidence {orient_conf.confidence:.2f} - {action}" + + +def get_orientation_correction(preview: Path, page_context: PageContext): + """Work out orientation correct for each page. We ask Ghostscript to draw a preview page, which will rasterize with the - current /Rotate applied, and then ask Tesseract which way the page is + current /Rotate applied, and then ask OCR which way the page is oriented. If the value of /Rotate is correct (e.g., a user already - manually fixed rotation), then Tesseract will say the page is pointing + manually fixed rotation), then OCR will say the page is pointing up and the correction is zero. Otherwise, the orientation found by - Tesseract represents the clockwise rotation, or the counterclockwise + OCR represents the clockwise rotation, or the counterclockwise correction to rotation. When we draw the real page for OCR, we rotate it by the CCW correction, - which points it (hopefully) upright. _weave.py takes care of the orienting + which points it (hopefully) upright. _graft.py takes care of the orienting the image and text layers. - """ - options = context.get_options() - page_pdf = next(ii for ii in infiles if ii.endswith('.page.pdf')) - - if not options.rotate_pages: - re_symlink(page_pdf, output_file, log) - return - preview = next(ii for ii in infiles if ii.endswith('.preview.jpg')) - - orient_conf = tesseract.get_orientation( - preview, - engine_mode=options.tesseract_oem, - timeout=options.tesseract_timeout, - log=log, + orient_conf = page_context.plugin_manager.hook.get_ocr_engine().get_orientation( + preview, page_context.options ) - direction = {0: '⇧', 90: '⇨', 180: '⇩', 270: '⇦'} - - pageno = page_number(page_pdf) - 1 - pdfinfo = context.get_pdfinfo() - existing_rotation = pdfinfo[pageno].rotation - correction = orient_conf.angle % 360 + log.info(describe_rotation(page_context, orient_conf, correction)) + if ( + orient_conf.confidence >= page_context.options.rotate_pages_threshold + and correction != 0 + ): + return correction - apply_correction = False - action = '' - if orient_conf.confidence >= options.rotate_pages_threshold: - if correction != 0: - apply_correction = True - action = ' - will rotate' - else: - action = ' - rotation appears correct' - else: - if correction != 0: - action = ' - confidence too low to rotate' - else: - action = ' - no change' - - facing = '' - if existing_rotation != 0: - facing = 'with existing rotation {}, '.format( - direction.get(existing_rotation, '?') - ) - facing += 'page is facing {}'.format(direction.get(orient_conf.angle, '?')) - - log.info( - '{pagenum:4d}: {facing}, confidence {conf:.2f}{action}'.format( - pagenum=page_number(preview), - facing=facing, - conf=orient_conf.confidence, - action=action, - ) - ) - - re_symlink(page_pdf, output_file, log) - if apply_correction: - context.set_rotation(pageno, correction) + return 0 -def rasterize_with_ghostscript(input_file, output_file, log, context): - options = context.get_options() - pageinfo = get_pageinfo(input_file, context) - +def rasterize( + input_file: Path, + page_context: PageContext, + correction: int = 0, + output_tag: str = '', + remove_vectors=None, +): colorspaces = ['pngmono', 'pnggray', 'png256', 'png16m'] device_idx = 0 + if remove_vectors is None: + remove_vectors = page_context.options.remove_vectors + + output_file = page_context.get_path(f'rasterize{output_tag}.png') + pageinfo = page_context.pageinfo + def at_least(cs): return max(device_idx, colorspaces.index(cs)) @@ -503,91 +438,73 @@ def rasterize_with_ghostscript(input_file, output_file, log, context): else: device_idx = at_least('png16m') + if pageinfo.has_vector: + device_idx = at_least('png16m') + device = colorspaces[device_idx] - log.debug(f"Rasterize {os.path.basename(input_file)} with {device}") + log.debug(f"Rasterize with {device}, rotation {correction}") # Produce the page image with square resolution or else deskew and OCR # will not work properly. - canvas_dpi = get_canvas_square_dpi(pageinfo, options) - page_dpi = get_page_square_dpi(pageinfo, options) + canvas_dpi = get_canvas_square_dpi(pageinfo, page_context.options) + page_dpi = get_page_square_dpi(pageinfo, page_context.options) - correction = context.get_rotation(page_number(input_file) - 1) + page_context.plugin_manager.hook.rasterize_pdf_page( + input_file=input_file, + output_file=output_file, + raster_device=device, + raster_dpi=canvas_dpi, + page_dpi=page_dpi, + pageno=pageinfo.pageno + 1, + rotation=correction, + filter_vector=remove_vectors, + ) + return output_file - ghostscript.rasterize_pdf( + +def preprocess_remove_background(input_file: Path, page_context: PageContext): + if any(image.bpc > 1 for image in page_context.pageinfo.images): + output_file = page_context.get_path('pp_rm_bg.png') + leptonica.remove_background(input_file, output_file) + return output_file + else: + log.info("background removal skipped on mono page") + return input_file + + +def preprocess_deskew(input_file: Path, page_context: PageContext): + output_file = page_context.get_path('pp_deskew.png') + dpi = get_page_square_dpi(page_context.pageinfo, page_context.options) + leptonica.deskew(input_file, output_file, dpi.x) + return output_file + + +def preprocess_clean(input_file: Path, page_context: PageContext): + output_file = page_context.get_path('pp_clean.png') + dpi = get_page_square_dpi(page_context.pageinfo, page_context.options) + unpaper.clean( input_file, output_file, - xres=canvas_dpi, - yres=canvas_dpi, - raster_device=device, - log=log, - page_dpi=(page_dpi, page_dpi), - pageno=page_number(input_file), - rotation=correction, - filter_vector=options.remove_vectors, + dpi=dpi.x, + unpaper_args=page_context.options.unpaper_args, ) + return output_file -def preprocess_remove_background(input_file, output_file, log, context): - options = context.get_options() - if not options.remove_background: - re_symlink(input_file, output_file, log) - return - - pageinfo = get_pageinfo(input_file, context) - - if any(image.bpc > 1 for image in pageinfo.images): - leptonica.remove_background(input_file, output_file) - else: - log.info(f"{pageinfo.pageno:4d}: background removal skipped on mono page") - re_symlink(input_file, output_file, log) - - -def preprocess_deskew(input_file, output_file, log, context): - options = context.get_options() - if not options.deskew: - re_symlink(input_file, output_file, log) - return - - pageinfo = get_pageinfo(input_file, context) - dpi = get_page_square_dpi(pageinfo, options) - - leptonica.deskew(input_file, output_file, dpi) - - -def preprocess_clean(input_file, output_file, log, context): - options = context.get_options() - if not options.clean: - re_symlink(input_file, output_file, log) - return - - from .exec import unpaper - - pageinfo = get_pageinfo(input_file, context) - dpi = get_page_square_dpi(pageinfo, options) - - unpaper.clean(input_file, output_file, dpi, log, options.unpaper_args) - - -def select_ocr_image(infiles, output_file, log, context): - """Select the image we send for OCR. May not be the same as the display +def create_ocr_image(image: Path, page_context: PageContext): + """Create the image we send for OCR. May not be the same as the display image depending on preprocessing. This image will never be shown to the user.""" - image = infiles[0] - options = context.get_options() - pageinfo = get_pageinfo(image, context) - + output_file = page_context.get_path('ocr.png') + options = page_context.options with Image.open(image) as im: - from PIL import ImageColor - from PIL import ImageDraw - white = ImageColor.getcolor('#ffffff', im.mode) # pink = ImageColor.getcolor('#ff0080', im.mode) draw = ImageDraw.ImageDraw(im) - xres, yres = im.info['dpi'] - log.debug('resolution %r %r', xres, yres) + log.debug('resolution %r', im.info['dpi']) if not options.force_ocr: # Do not mask text areas when forcing OCR, because we need to OCR @@ -596,170 +513,154 @@ def select_ocr_image(infiles, output_file, log, context): if options.redo_ocr: mask = True # Mask visible text, but not invisible text - for textarea in pageinfo.get_textareas(visible=mask, corrupt=None): + for textarea in page_context.pageinfo.get_textareas( + visible=mask, corrupt=None + ): # Calculate resolution based on the image size and page dimensions # without regard whatever resolution is in pageinfo (may differ or # be None) bbox = [float(v) for v in textarea] - xscale, yscale = float(xres) / 72.0, float(yres) / 72.0 + xyscale = tuple(float(coord) / 72.0 for coord in im.info['dpi']) pixcoords = [ - bbox[0] * xscale, - im.height - bbox[3] * yscale, - bbox[2] * xscale, - im.height - bbox[1] * yscale, + bbox[0] * xyscale[0], + im.height - bbox[3] * xyscale[1], + bbox[2] * xyscale[0], + im.height - bbox[1] * xyscale[1], ] pixcoords = [int(round(c)) for c in pixcoords] log.debug('blanking %r', pixcoords) draw.rectangle(pixcoords, fill=white) # draw.rectangle(pixcoords, outline=pink) - if options.mask_barcodes or options.threshold: + if options.threshold: pix = leptonica.Pix.frompil(im) - if options.threshold: - pix = pix.masked_threshold_on_background_norm() - if options.mask_barcodes: - barcodes = pix.locate_barcodes() - for barcode in barcodes: - decoded, rect = barcode - log.info('masking barcode %s %r', decoded, rect) - draw.rectangle(rect, fill=white) - im = pix.topil() + pix = pix.masked_threshold_on_background_norm() + im_pix = pix.topil() + im_pix.info['dpi'] = im.info['dpi'] + im = im_pix del draw - # Pillow requires integer DPI - dpi = round(xres), round(yres) - im.save(output_file, dpi=dpi) - -def ocr_tesseract_hocr(input_file, output_files, log, context): - options = context.get_options() - tesseract.generate_hocr( - input_file=input_file, - output_files=output_files, - language=options.language, - engine_mode=options.tesseract_oem, - tessconfig=options.tesseract_config, - timeout=options.tesseract_timeout, - pagesegmode=options.tesseract_pagesegmode, - user_words=options.user_words, - user_patterns=options.user_patterns, - log=log, - ) - - -def select_visible_page_image(infiles, output_file, log, context): - """Selects a whole page image that we can show the user (if necessary)""" - - options = context.get_options() - if options.clean_final: - image_suffix = '.pp-clean.png' - elif options.deskew: - image_suffix = '.pp-deskew.png' - elif options.remove_background: - image_suffix = '.pp-background.png' - else: - image_suffix = '.page.png' - image = next(ii for ii in infiles if ii.endswith(image_suffix)) - - pageinfo = get_pageinfo(image, context) - if pageinfo.images and all(im.enc == 'jpeg' for im in pageinfo.images): - log.debug(f'{page_number(image):4d}: JPEG input -> JPEG output') - # If all images were JPEGs originally, produce a JPEG as output - with Image.open(image) as im: - # At this point the image should be a .png, but deskew, unpaper - # might have removed the DPI information. In this case, fall back to - # square DPI used to rasterize. When the preview image was - # rasterized, it was also converted to square resolution, which is - # what we want to give tesseract, so keep it square. - fallback_dpi = get_page_square_dpi(pageinfo, options) - dpi = im.info.get('dpi', (fallback_dpi, fallback_dpi)) - - # Pillow requires integer DPI - dpi = round(dpi[0]), round(dpi[1]) - im.save(output_file, format='JPEG', dpi=dpi) - else: - re_symlink(image, output_file, log) - - -def select_image_layer(infiles, output_file, log, context): - """Selects the image layer for the output page. If possible this is the - orientation-corrected input page, or an image of the whole page converted - to PDF.""" - - options = context.get_options() - page_pdf = next(ii for ii in infiles if ii.endswith('.ocr.oriented.pdf')) - image = next(ii for ii in infiles if ii.endswith('.image')) - - if options.lossless_reconstruction: - log.debug( - f"{page_number(page_pdf):4d}: page eligible for lossless reconstruction" + filter_im = page_context.plugin_manager.hook.filter_ocr_image( + page=page_context, image=im ) - re_symlink(page_pdf, output_file, log) # Still points to multipage - return + if filter_im is not None: + im = filter_im - pageinfo = get_pageinfo(image, context) + # Pillow requires integer DPI + dpi = tuple(round(coord) for coord in im.info['dpi']) + im.save(output_file, dpi=dpi) + return output_file + +def ocr_engine_hocr(input_file: Path, page_context: PageContext): + hocr_out = page_context.get_path('ocr_hocr.hocr') + hocr_text_out = page_context.get_path('ocr_hocr.txt') + options = page_context.options + + ocr_engine = page_context.plugin_manager.hook.get_ocr_engine() + ocr_engine.generate_hocr( + input_file=input_file, + output_hocr=hocr_out, + output_text=hocr_text_out, + options=options, + ) + return (hocr_out, hocr_text_out) + + +def should_visible_page_image_use_jpg(pageinfo): + # If all images were JPEGs originally, produce a JPEG as output + return pageinfo.images and all(im.enc == Encoding.jpeg for im in pageinfo.images) + + +def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path: + output_file = page_context.get_path('visible.jpg') + with Image.open(image) as im: + # At this point the image should be a .png, but deskew, unpaper + # might have removed the DPI information. In this case, fall back to + # square DPI used to rasterize. When the preview image was + # rasterized, it was also converted to square resolution, which is + # what we want to give to the OCR engine, so keep it square. + if 'dpi' in im.info: + dpi = Resolution(*im.info['dpi']) + else: + # Fallback to page-implied DPI + dpi = get_page_square_dpi(page_context.pageinfo, page_context.options) + + # Pillow requires integer DPI + im.save(output_file, format='JPEG', dpi=dpi.to_int()) + return output_file + + +def create_pdf_page_from_image( + image: Path, page_context: PageContext, orientation_correction +): # 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 # accurately describes the image. It would be possible to resample the image # at this stage back to non-square DPI to more closely resemble the input, # except that the hocr renderer does not understand non-square DPI. The # sandwich renderer would be fine. - dpi = get_page_square_dpi(pageinfo, options) - layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi)) + output_file = page_context.get_path('visible.pdf') + + pageinfo = page_context.pageinfo + pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches) + effective_rotation = (pageinfo.rotation - orientation_correction) % 360 + if effective_rotation % 180 == 90: + pagesize = pagesize[1], pagesize[0] # This create a single page PDF with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf: - log.debug(f'{page_number(page_pdf):4d}: convert') + log.debug('convert') + + layout_fun = img2pdf.get_layout_fun(pagesize) img2pdf.convert( imfile, with_pdfrw=False, layout_fun=layout_fun, outputstream=pdf ) - log.debug(f'{page_number(page_pdf):4d}: convert done') + log.debug('convert done') - -def render_hocr_page(infiles, output_file, log, context): - options = context.get_options() - hocr = next(ii for ii in infiles if ii.endswith('.hocr')) - pageinfo = get_pageinfo(hocr, context) - dpi = get_page_square_dpi(pageinfo, options) - - hocrtransform = HocrTransform(hocr, dpi) - hocrtransform.to_pdf( - output_file, - imageFileName=None, - showBoundingboxes=False, - invisibleText=True, - interwordSpaces=True, + output_file = page_context.plugin_manager.hook.filter_pdf_page( + page=page_context, image_filename=image, output_pdf=output_file ) + return output_file -def ocr_tesseract_textonly_pdf(infiles, outfiles, log, context): - options = context.get_options() - input_image = next((ii for ii in infiles if ii.endswith('.ocr.png')), '') - if not input_image: - raise ValueError("No image rendered?") - output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf'))) - output_text = next((ii for ii in outfiles if ii.endswith('.txt'))) +def render_hocr_page(hocr: Path, page_context: PageContext): + options = page_context.options + output_file = page_context.get_path('ocr_hocr.pdf') + dpi = get_page_square_dpi(page_context.pageinfo, options) + debug_mode = options.pdf_renderer == 'hocrdebug' - tesseract.generate_pdf( - input_image=input_image, - skip_pdf=None, + hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square + hocrtransform.to_pdf( + out_filename=output_file, + image_filename=None, + show_bounding_boxes=False if not debug_mode else True, + invisible_text=True if not debug_mode else False, + interword_spaces=True, + ) + return output_file + + +def ocr_engine_textonly_pdf(input_image: Path, page_context: PageContext): + output_pdf = page_context.get_path('ocr_tess.pdf') + output_text = page_context.get_path('ocr_tess.txt') + options = page_context.options + + ocr_engine = page_context.plugin_manager.hook.get_ocr_engine() + ocr_engine.generate_pdf( + input_file=input_image, output_pdf=output_pdf, output_text=output_text, - language=options.language, - engine_mode=options.tesseract_oem, - text_only=True, - tessconfig=options.tesseract_config, - timeout=options.tesseract_timeout, - pagesegmode=options.tesseract_pagesegmode, - user_words=options.user_words, - user_patterns=options.user_patterns, - log=log, + options=options, ) + return (output_pdf, output_text) -def get_docinfo(base_pdf, options): +def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]: + options = context.options + def from_document_info(key): try: s = base_pdf.docinfo[key] @@ -771,398 +672,222 @@ def get_docinfo(base_pdf, options): k: from_document_info(k) for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate') } - if options.title: - pdfmark['/Title'] = options.title - if options.author: - pdfmark['/Author'] = options.author - if options.keywords: - pdfmark['/Keywords'] = options.keywords - if options.subject: - pdfmark['/Subject'] = options.subject + if options is not None: + if options.title: + pdfmark['/Title'] = options.title + if options.author: + pdfmark['/Author'] = options.author + if options.keywords: + pdfmark['/Keywords'] = options.keywords + if options.subject: + pdfmark['/Subject'] = options.subject - if options.pdf_renderer == 'sandwich': - renderer_tag = 'OCR-PDF' - else: - renderer_tag = 'OCR' + creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options) - pdfmark['/Creator'] = ( - f'{PROGRAM_NAME} {VERSION} / ' f'Tesseract {renderer_tag} {tesseract.version()}' - ) + pdfmark['/Creator'] = f'{PROGRAM_NAME} {VERSION} / {creator_tag}' pdfmark['/Producer'] = f'pikepdf {pikepdf.__version__}' - if 'OCRMYPDF_CREATOR' in os.environ: - pdfmark['/Creator'] = os.environ['OCRMYPDF_CREATOR'] - if 'OCRMYPDF_PRODUCER' in os.environ: - pdfmark['/Producer'] = os.environ['OCRMYPDF_PRODUCER'] - pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc)) return pdfmark -def generate_postscript_stub(input_file, output_file, log, context): +def generate_postscript_stub(context: PdfContext): + output_file = context.get_path('pdfa.ps') generate_pdfa_ps(output_file) + return output_file -def convert_to_pdfa(input_files_groups, output_file, log, context): - options = context.get_options() - input_pdfinfo = context.get_pdfinfo() - - input_files = list(f for f in flatten_groups(input_files_groups)) - layers_file = next( - (ii for ii in input_files if ii.endswith('layers.rendered.pdf')), None - ) +def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext): + options = context.options + input_pdfinfo = context.pdfinfo + fix_docinfo_file = context.get_path('fix_docinfo.pdf') + output_file = context.get_path('pdfa.pdf') # If the DocumentInfo record contains NUL characters, Ghostscript will # produce XMP metadata which contains invalid XML entities (�). # 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 # stamping them out as soon as possible. - with pikepdf.open(layers_file) as pdf_layers_file: - if pdf_layers_file.docinfo: - modified = False - for k, v in pdf_layers_file.docinfo.items(): - if b'\x00' in bytes(v): - pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'') - modified = True - if modified: - pdf_layers_file.save(layers_file) + modified = False + with pikepdf.open(input_pdf) as pdf_file: + try: + 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) + else: + safe_symlink(input_pdf, fix_docinfo_file) - ps = next((ii for ii in input_files if ii.endswith('.ps')), None) - ghostscript.generate_pdfa( + context.plugin_manager.hook.generate_pdfa( pdf_version=input_pdfinfo.min_version, - pdf_pages=[layers_file, ps], + pdf_pages=[fix_docinfo_file], + pdfmark=input_ps_stub, output_file=output_file, compression=options.pdfa_image_compression, - log=log, - threads=options.jobs or 1, pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3 + progressbar_class=( + context.plugin_manager.hook.get_progressbar_class() + if options.progress_bar + else None + ), ) + return output_file -def metadata_fixup(input_files_groups, output_file, log, context): - options = context.get_options() - input_files = list(f for f in flatten_groups(input_files_groups)) - original_file = next( - (ii for ii in input_files if ii.endswith('.repaired.pdf')), None +def should_linearize(working_file: Path, context: PdfContext): + filesize = os.stat(working_file).st_size + if filesize > (context.options.fast_web_view * 1_000_000): + return True + return False + + +def get_pdf_save_settings(output_type: str): + if output_type == 'pdfa-1': + # Trigger recompression to ensure object streams are removed, because + # Acrobat complains about them in PDF/A-1b validation. + return dict( + preserve_pdfa=True, + compress_streams=True, + stream_decode_level=pikepdf.StreamDecodeLevel.generalized, + object_stream_mode=pikepdf.ObjectStreamMode.disable, + ) + else: + return dict( + preserve_pdfa=True, + compress_streams=True, + object_stream_mode=(pikepdf.ObjectStreamMode.generate), + ) + + +def metadata_fixup(working_file: Path, context: PdfContext): + output_file = context.get_path('metafix.pdf') + options = context.options + + def report_on_metadata(missing): + if not missing: + return + if options.output_type.startswith('pdfa'): + log.warning( + "Some input metadata could not be copied because it is not " + "permitted in PDF/A. You may wish to examine the output " + "PDF's XMP metadata." + ) + log.debug("The following metadata fields were not copied: %r", missing) + else: + log.error( + "Some input metadata could not be copied." + "You may wish to examine the output PDF's XMP metadata." + ) + log.info("The following metadata fields were not copied: %r", missing) + + with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf: + docinfo = get_docinfo(original, context) + with pdf.open_metadata() as meta: + meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False) + # If xmp:CreateDate is missing, set it to the modify date to + # match Ghostscript, for consistency + if 'xmp:CreateDate' not in meta: + meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '') + + with original.open_metadata( + set_pikepdf_as_editor=False, update_docinfo=False, strict=False + ) as meta_original: + if meta.get('dc:title') == 'Untitled': + # Ghostscript likes to set title to Untitled if omitted from input. + # Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1 + # and the XMP Spec do not make this recommendation. + if 'dc:title' not in meta_original: + del meta['dc:title'] + missing = set(meta_original.keys()) - set(meta.keys()) + report_on_metadata(missing) + + pdf.save( + output_file, + **get_pdf_save_settings(options.output_type), + linearize=( # Don't linearize if optimize() will be linearizing too + should_linearize(working_file, context) + if options.optimize == 0 + else False + ), + ) + + return output_file + + +def optimize_pdf(input_file: Path, context: PdfContext, executor: Executor): + output_file = context.get_path('optimize.pdf') + save_settings = dict( + linearize=should_linearize(input_file, context), + **get_pdf_save_settings(context.options.output_type), ) - layers_file = next( - (ii for ii in input_files if ii.endswith('layers.rendered.pdf')), None - ) - pdfa_file = next((ii for ii in input_files if ii.endswith('pdfa.pdf')), None) - original = pikepdf.open(original_file) - docinfo = get_docinfo(original, options) - - working_file = pdfa_file if pdfa_file else layers_file - - pdf = pikepdf.open(working_file) - with pdf.open_metadata() as meta: - meta.load_from_docinfo(docinfo, delete_missing=False) - # If xmp:CreateDate is missing, set it to the modify date to - # match Ghostscript, for consistency - if 'xmp:CreateDate' not in meta: - meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '') - if pdfa_file: - meta_original = original.open_metadata() - not_copied = set(meta_original.keys()) - set(meta.keys()) - if not_copied: - log.warning( - "Some input metadata could not be copied because it is not " - "permitted in PDF/A. You may wish to examine the output " - "PDF's XMP metadata." - ) - log.debug( - "The following metadata fields were not copied: %r", not_copied - ) - - pdf.save( - output_file, - compress_streams=True, - object_stream_mode=pikepdf.ObjectStreamMode.generate, - ) - original.close() - pdf.close() + optimize(input_file, output_file, context, save_settings, executor) + return output_file -def optimize_pdf(input_file, output_file, log, context): - optimize(input_file, output_file, log, context) +def enumerate_compress_ranges(iterable): + skipped_from, index = None, None + for index, txt_file in enumerate(iterable): + index += 1 + if txt_file: + if skipped_from is not None: + yield (skipped_from, index - 1), None + skipped_from = None + yield (index, index), txt_file + else: + if skipped_from is None: + skipped_from = index + if skipped_from is not None: + yield (skipped_from, index), None -def merge_sidecars(input_files_groups, output_file, log, context): - pdfinfo = context.get_pdfinfo() - - txt_files = [None] * len(pdfinfo) - - for infile in flatten_groups(input_files_groups): - if infile.endswith('.txt'): - idx = page_number(infile) - 1 - txt_files[idx] = infile - - def write_pages(stream): - for page_num, txt_file in enumerate(txt_files): - if page_num != 0: +def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext): + output_file = context.get_path('sidecar.txt') + with open(output_file, 'w', encoding="utf-8") as stream: + for (frm, to), txt_file in enumerate_compress_ranges(txt_files): + if frm != 1: stream.write('\f') # Form feed between pages if txt_file: with open(txt_file, 'r', encoding="utf-8") as in_: txt = in_.read() - # Tesseract v4 alpha started adding form feeds in - # commit aa6eb6b - # No obvious way to detect what binaries will do this, so - # for consistency just ignore its form feeds and insert our - # own + # Some OCR engines (e.g. Tesseract v4 alpha) add form feeds + # between pages, and some do not. For consistency, we ignore + # any added by the OCR engine and them on our own. if txt.endswith('\f'): stream.write(txt[:-1]) else: stream.write(txt) else: - stream.write(f'[OCR skipped on page {(page_num + 1)}]') - - if output_file == '-': - write_pages(sys.stdout) - sys.stdout.flush() - else: - with open(output_file, 'w', encoding="utf-8") as out: - write_pages(out) + if frm != to: + pages = f'{frm}-{to}' + else: + pages = f'{frm}' + stream.write(f'[OCR skipped on page(s) {pages}]') + return output_file -def copy_final(input_files, output_file, log, context): - input_file = next((ii for ii in input_files if ii.endswith('.pdf'))) +def copy_final(input_file, output_file, _context: PdfContext): log.debug('%s -> %s', input_file, output_file) with open(input_file, 'rb') as input_stream: if output_file == '-': copyfileobj(input_stream, sys.stdout.buffer) sys.stdout.flush() + elif hasattr(output_file, 'writable'): + output_stream = output_file + copyfileobj(input_stream, output_stream) + with suppress(AttributeError): + output_stream.flush() else: # At this point we overwrite the output_file specified by the user # use copyfileobj because then we use open() to create the file and # get the appropriate umask, ownership, etc. with open(output_file, 'wb') as output_stream: copyfileobj(input_stream, output_stream) - - -def build_pipeline(options, work_folder, log, context): - main_pipeline = Pipeline.pipelines['main'] - - # Triage - task_triage = main_pipeline.transform( - task_func=triage, - input=os.path.join(work_folder, 'origin'), - filter=formatter('(?i)'), - output=os.path.join(work_folder, 'origin.pdf'), - extras=[log, context], - ) - - task_repair_and_parse_pdf = main_pipeline.transform( - task_func=repair_and_parse_pdf, - input=task_triage, - filter=suffix('.pdf'), - output='.repaired.pdf', - output_dir=work_folder, - extras=[log, context], - ) - - # Split (kwargs for split seems to be broken, so pass plain args) - task_marker_pages = main_pipeline.split( - marker_pages, - task_repair_and_parse_pdf, - os.path.join(work_folder, '*.marker.pdf'), - extras=[log, context], - ) - - task_ocr_or_skip = main_pipeline.split( - ocr_or_skip, - task_marker_pages, - [ - os.path.join(work_folder, '*.ocr.page.pdf'), - os.path.join(work_folder, '*.skip.page.pdf'), - ], - extras=[log, context], - ) - - # Rasterize preview - task_rasterize_preview = main_pipeline.transform( - task_func=rasterize_preview, - input=task_ocr_or_skip, - filter=suffix('.page.pdf'), - output='.preview.jpg', - output_dir=work_folder, - extras=[log, context], - ) - task_rasterize_preview.active_if(options.rotate_pages) - - # Orient - task_orient_page = main_pipeline.collate( - task_func=orient_page, - input=[task_ocr_or_skip, task_rasterize_preview], - filter=regex(r".*/(\d{6})(\.ocr|\.skip)(?:\.page\.pdf|\.preview\.jpg)"), - output=os.path.join(work_folder, r'\1\2.oriented.pdf'), - extras=[log, context], - ) - - # Rasterize actual - task_rasterize_with_ghostscript = main_pipeline.transform( - task_func=rasterize_with_ghostscript, - input=task_orient_page, - filter=suffix('.ocr.oriented.pdf'), - output='.page.png', - output_dir=work_folder, - extras=[log, context], - ) - - # Preprocessing subpipeline - task_preprocess_remove_background = main_pipeline.transform( - task_func=preprocess_remove_background, - input=task_rasterize_with_ghostscript, - filter=suffix(".page.png"), - output=".pp-background.png", - extras=[log, context], - ) - - task_preprocess_deskew = main_pipeline.transform( - task_func=preprocess_deskew, - input=task_preprocess_remove_background, - filter=suffix(".pp-background.png"), - output=".pp-deskew.png", - extras=[log, context], - ) - - task_preprocess_clean = main_pipeline.transform( - task_func=preprocess_clean, - input=task_preprocess_deskew, - filter=suffix(".pp-deskew.png"), - output=".pp-clean.png", - extras=[log, context], - ) - - task_select_ocr_image = main_pipeline.collate( - task_func=select_ocr_image, - input=[task_preprocess_clean], - filter=regex(r".*/(\d{6})(?:\.page|\.pp-.*)\.png"), - output=os.path.join(work_folder, r"\1.ocr.png"), - extras=[log, context], - ) - - # HOCR OCR - task_ocr_tesseract_hocr = main_pipeline.transform( - task_func=ocr_tesseract_hocr, - input=task_select_ocr_image, - filter=suffix(".ocr.png"), - output=[".hocr", ".txt"], - extras=[log, context], - ) - task_ocr_tesseract_hocr.graphviz(fillcolor='"#00cc66"') - task_ocr_tesseract_hocr.active_if(options.pdf_renderer == 'hocr') - - task_select_visible_page_image = main_pipeline.collate( - task_func=select_visible_page_image, - input=[ - task_rasterize_with_ghostscript, - task_preprocess_remove_background, - task_preprocess_deskew, - task_preprocess_clean, - ], - filter=regex(r".*/(\d{6})(?:\.page|\.pp-.*)\.png"), - output=os.path.join(work_folder, r'\1.image'), - extras=[log, context], - ) - task_select_visible_page_image.graphviz(shape='diamond') - - task_select_image_layer = main_pipeline.collate( - task_func=select_image_layer, - input=[task_select_visible_page_image, task_orient_page], - filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"), - output=os.path.join(work_folder, r'\1.image-layer.pdf'), - extras=[log, context], - ) - task_select_image_layer.graphviz(fillcolor='"#00cc66"', shape='diamond') - - task_render_hocr_page = main_pipeline.transform( - task_func=render_hocr_page, - input=task_ocr_tesseract_hocr, - filter=regex(r".*/(\d{6})(?:\.hocr)"), - output=os.path.join(work_folder, r'\1.text.pdf'), - extras=[log, context], - ) - task_render_hocr_page.graphviz(fillcolor='"#00cc66"') - task_render_hocr_page.active_if(options.pdf_renderer == 'hocr') - - # Tesseract OCR + text only PDF - task_ocr_tesseract_textonly_pdf = main_pipeline.collate( - task_func=ocr_tesseract_textonly_pdf, - input=[task_select_ocr_image], - filter=regex(r".*/(\d{6})(?:\.ocr.png)"), - output=[ - os.path.join(work_folder, r'\1.text.pdf'), - os.path.join(work_folder, r'\1.text.txt'), - ], - extras=[log, context], - ) - task_ocr_tesseract_textonly_pdf.graphviz(fillcolor='"#ff69b4"') - task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'sandwich') - - task_weave_layers = main_pipeline.collate( - task_func=weave_layers, - input=[ - task_repair_and_parse_pdf, - task_render_hocr_page, - task_ocr_tesseract_textonly_pdf, - task_select_image_layer, - ], - filter=regex( - r".*/((?:\d{6}(?:\.text\.pdf|\.image-layer\.pdf))|(?:origin\.repaired\.pdf))" - ), - output=os.path.join(work_folder, r'layers.rendered.pdf'), - extras=[log, context], - ) - task_weave_layers.graphviz(fillcolor='"#00cc66"') - - # PDF/A pdfmark - task_generate_postscript_stub = main_pipeline.transform( - task_func=generate_postscript_stub, - input=task_repair_and_parse_pdf, - filter=formatter(r'\.repaired\.pdf'), - output=os.path.join(work_folder, 'pdfa.ps'), - extras=[log, context], - ) - task_generate_postscript_stub.active_if(options.output_type.startswith('pdfa')) - - # PDF/A conversion - task_convert_to_pdfa = main_pipeline.merge( - task_func=convert_to_pdfa, - input=[task_generate_postscript_stub, task_weave_layers], - output=os.path.join(work_folder, 'pdfa.pdf'), - extras=[log, context], - ) - task_convert_to_pdfa.active_if(options.output_type.startswith('pdfa')) - - task_metadata_fixup = main_pipeline.merge( - task_func=metadata_fixup, - input=[task_repair_and_parse_pdf, task_weave_layers, task_convert_to_pdfa], - output=os.path.join(work_folder, 'metafix.pdf'), - extras=[log, context], - ) - - task_merge_sidecars = main_pipeline.merge( - task_func=merge_sidecars, - input=[task_ocr_tesseract_hocr, task_ocr_tesseract_textonly_pdf], - output=options.sidecar, - extras=[log, context], - ) - task_merge_sidecars.active_if(options.sidecar) - - # Optimize - task_optimize_pdf = main_pipeline.transform( - task_func=optimize_pdf, - input=task_metadata_fixup, - filter=suffix('.pdf'), - output='.optimized.pdf', - output_dir=work_folder, - extras=[log, context], - ) - - # Finalize - main_pipeline.merge( - task_func=copy_final, - input=[task_optimize_pdf], - output=options.output_file, - extras=[log, context], - ) diff --git a/src/ocrmypdf/_plugin_manager.py b/src/ocrmypdf/_plugin_manager.py new file mode 100644 index 00000000..0ac14578 --- /dev/null +++ b/src/ocrmypdf/_plugin_manager.py @@ -0,0 +1,122 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import argparse +import importlib +import importlib.util +import pkgutil +import sys +from pathlib import Path +from typing import List, Tuple, Union + +import pluggy + +import ocrmypdf.builtin_plugins +from ocrmypdf import pluginspec +from ocrmypdf.cli import get_parser, plugins_only_parser + + +class OcrmypdfPluginManager(pluggy.PluginManager): + """pluggy.PluginManager that can fork. + + Capable of reconstructing itself in child workers. + + Arguments: + setup_func: callback that initializes the plugin manager with all + standard plugins + """ + + def __init__( + self, + *args, + plugins: List[Union[str, Path]], + builtins: bool = True, + **kwargs, + ): + self.__init_args = args + self.__init_kwargs = kwargs + self.__plugins = plugins + self.__builtins = builtins + super().__init__(*args, **kwargs) + self.setup_plugins() + + def __getstate__(self): + state = dict( + init_args=self.__init_args, + plugins=self.__plugins, + builtins=self.__builtins, + init_kwargs=self.__init_kwargs, + ) + return state + + def __setstate__(self, state): + self.__init__( + *state['init_args'], + plugins=state['plugins'], + builtins=state['builtins'], + **state['init_kwargs'], + ) + + def setup_plugins(self): + self.add_hookspecs(pluginspec) + + # 1. Register builtins + if self.__builtins: + for module in sorted( + pkgutil.iter_modules(ocrmypdf.builtin_plugins.__path__) + ): + name = f'ocrmypdf.builtin_plugins.{module.name}' + module = importlib.import_module(name) + self.register(module) + + # 2. Install semfree if needed + try: + # pylint: disable=import-outside-toplevel + from multiprocessing.synchronize import SemLock + + del SemLock + except ImportError: + self.register(importlib.import_module('ocrmypdf.extra_plugins.semfree')) + + # 3. Register setuptools plugins + self.load_setuptools_entrypoints('ocrmypdf') + + # 4. Register plugins specified on command line + for name in self.__plugins: + if isinstance(name, Path) or name.endswith('.py'): + # Import by filename + module_name = Path(name).stem + spec = importlib.util.spec_from_file_location(module_name, name) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + else: + # Import by dotted module name + module = importlib.import_module(name) + self.register(module) + + +def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True): + pm = OcrmypdfPluginManager( + project_name='ocrmypdf', + plugins=plugins, + builtins=builtins, + ) + return pm + + +def get_parser_options_plugins( + args, +) -> Tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]: + pre_options, _unused = plugins_only_parser.parse_known_args(args=args) + plugin_manager = get_plugin_manager(pre_options.plugins) + + parser = get_parser() + plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member + + options = parser.parse_args(args=args) + return parser, options, plugin_manager diff --git a/src/ocrmypdf/_sync.py b/src/ocrmypdf/_sync.py new file mode 100644 index 00000000..d9432cf4 --- /dev/null +++ b/src/ocrmypdf/_sync.py @@ -0,0 +1,426 @@ +# © 2016 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import logging.handlers +import os +import sys +import threading +from functools import partial +from pathlib import Path +from tempfile import mkdtemp +from typing import List, NamedTuple, Optional, Tuple + +import PIL + +from ocrmypdf._concurrent import Executor, setup_executor +from ocrmypdf._graft import OcrGrafter +from ocrmypdf._jobcontext import PageContext, PdfContext, cleanup_working_files +from ocrmypdf._logging import PageNumberFilter +from ocrmypdf._pipeline import ( + convert_to_pdfa, + copy_final, + create_ocr_image, + create_pdf_page_from_image, + create_visible_page_jpg, + generate_postscript_stub, + get_orientation_correction, + get_pdfinfo, + is_ocr_required, + merge_sidecars, + metadata_fixup, + ocr_engine_hocr, + ocr_engine_textonly_pdf, + optimize_pdf, + preprocess_clean, + preprocess_deskew, + preprocess_remove_background, + rasterize, + rasterize_preview, + render_hocr_page, + should_visible_page_image_use_jpg, + triage, + validate_pdfinfo_options, +) +from ocrmypdf._plugin_manager import get_plugin_manager +from ocrmypdf._validation import ( + check_requested_output_file, + create_input_file, + report_output_file_size, +) +from ocrmypdf.exceptions import ExitCode, ExitCodeException +from ocrmypdf.helpers import ( + NeverRaise, + available_cpu_count, + check_pdf, + pikepdf_enable_mmap, + samefile, +) +from ocrmypdf.pdfa import file_claims_pdfa + +log = logging.getLogger(__name__) + + +class PageResult(NamedTuple): # pylint: disable=inherit-non-class + pageno: int + pdf_page_from_image: Optional[Path] + ocr: Optional[Path] + text: Optional[Path] + orientation_correction: int + + +tls = threading.local() +tls.pageno = None + + +old_factory = logging.getLogRecordFactory() + + +def record_factory(*args, **kwargs): + record = old_factory(*args, **kwargs) + if hasattr(tls, 'pageno'): + record.pageno = tls.pageno + return record + + +logging.setLogRecordFactory(record_factory) + + +def preprocess( + page_context: PageContext, + image: Path, + remove_background: bool, + deskew: bool, + clean: bool, +) -> Path: + if remove_background: + image = preprocess_remove_background(image, page_context) + if deskew: + image = preprocess_deskew(image, page_context) + if clean: + image = preprocess_clean(image, page_context) + return image + + +def make_intermediate_images( + page_context: PageContext, orientation_correction: int +) -> Tuple[Path, Optional[Path]]: + options = page_context.options + + ocr_image = preprocess_out = None + rasterize_out = rasterize( + page_context.origin, + page_context, + correction=orientation_correction, + remove_vectors=False, + ) + + if not any([options.clean, options.clean_final, options.remove_vectors]): + ocr_image = preprocess_out = preprocess( + page_context, + rasterize_out, + options.remove_background, + options.deskew, + clean=False, + ) + else: + if not options.lossless_reconstruction: + preprocess_out = preprocess( + page_context, + rasterize_out, + options.remove_background, + options.deskew, + clean=options.clean_final, + ) + if options.remove_vectors: + rasterize_ocr_out = rasterize( + page_context.origin, + page_context, + correction=orientation_correction, + remove_vectors=True, + output_tag='_ocr', + ) + else: + rasterize_ocr_out = rasterize_out + + if ( + preprocess_out + and rasterize_ocr_out == rasterize_out + and options.clean == options.clean_final + ): + # Optimization: image for OCR is identical to presentation image + ocr_image = preprocess_out + else: + ocr_image = preprocess( + page_context, + rasterize_ocr_out, + options.remove_background, + options.deskew, + clean=options.clean, + ) + return ocr_image, preprocess_out + + +def exec_page_sync(page_context: PageContext): + options = page_context.options + tls.pageno = page_context.pageno + 1 + + if not is_ocr_required(page_context): + return PageResult( + pageno=page_context.pageno, + pdf_page_from_image=None, + ocr=None, + text=None, + orientation_correction=0, + ) + + orientation_correction = 0 + if options.rotate_pages: + # Rasterize + rasterize_preview_out = rasterize_preview(page_context.origin, page_context) + orientation_correction = get_orientation_correction( + rasterize_preview_out, page_context + ) + + ocr_image, preprocess_out = make_intermediate_images( + page_context, orientation_correction + ) + ocr_image_out = create_ocr_image(ocr_image, page_context) + + pdf_page_from_image_out = None + if not options.lossless_reconstruction: + assert preprocess_out + visible_image_out = preprocess_out + if should_visible_page_image_use_jpg(page_context.pageinfo): + visible_image_out = create_visible_page_jpg(visible_image_out, page_context) + filtered_image = page_context.plugin_manager.hook.filter_page_image( + page=page_context, image_filename=visible_image_out + ) + if filtered_image: + visible_image_out = filtered_image + pdf_page_from_image_out = create_pdf_page_from_image( + visible_image_out, page_context, orientation_correction + ) + + if options.pdf_renderer.startswith('hocr'): + (hocr_out, text_out) = ocr_engine_hocr(ocr_image_out, page_context) + ocr_out = render_hocr_page(hocr_out, page_context) + elif options.pdf_renderer == 'sandwich': + (ocr_out, text_out) = ocr_engine_textonly_pdf(ocr_image_out, page_context) + else: + raise NotImplementedError(f"pdf_renderer {options.pdf_renderer}") + + return PageResult( + pageno=page_context.pageno, + pdf_page_from_image=pdf_page_from_image_out, + ocr=ocr_out, + text=text_out, + orientation_correction=orientation_correction, + ) + + +def post_process(pdf_file, context: PdfContext, executor: Executor): + pdf_out = pdf_file + if context.options.output_type.startswith('pdfa'): + ps_stub_out = generate_postscript_stub(context) + pdf_out = convert_to_pdfa(pdf_out, ps_stub_out, context) + + pdf_out = metadata_fixup(pdf_out, context) + return optimize_pdf(pdf_out, context, executor) + + +def worker_init(max_pixels: int): + # 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 + # threaded, but harmless to set again. + PIL.Image.MAX_IMAGE_PIXELS = max_pixels + pikepdf_enable_mmap() + + +def exec_concurrent(context: PdfContext, executor: Executor): + """Execute the pipeline concurrently""" + + # Run exec_page_sync on every page context + options = context.options + max_workers = min(len(context.pdfinfo), options.jobs) + if max_workers > 1: + log.info("Start processing %d pages concurrently", max_workers) + + sidecars: List[Optional[Path]] = [None] * len(context.pdfinfo) + ocrgraft = OcrGrafter(context) + + def update_page(result: PageResult, pbar): + try: + tls.pageno = result.pageno + 1 + sidecars[result.pageno] = result.text + pbar.update() + ocrgraft.graft_page( + pageno=result.pageno, + image=result.pdf_page_from_image, + textpdf=result.ocr, + autorotate_correction=result.orientation_correction, + ) + pbar.update() + finally: + tls.pageno = None + + executor( + use_threads=options.use_threads, + max_workers=max_workers, + tqdm_kwargs=dict( + total=(2 * len(context.pdfinfo)), + desc='OCR' if options.tesseract_timeout > 0 else 'Image processing', + unit='page', + unit_scale=0.5, + disable=not options.progress_bar, + ), + worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS), + task=exec_page_sync, + task_arguments=context.get_page_contexts(), + task_finished=update_page, + ) + + # Output sidecar text + if options.sidecar: + text = merge_sidecars(sidecars, context) + # Copy text file to destination + copy_final(text, options.sidecar, context) + + # Merge layers to one single pdf + pdf = ocrgraft.finalize() + + # PDF/A and metadata + log.info("Postprocessing...") + pdf = post_process(pdf, context, executor) + + # Copy PDF file to destination + copy_final(pdf, options.output_file, context) + + +def configure_debug_logging(log_filename: Path, prefix: str = ''): + """ + Create a debug log file at a specified location. + + Arguments: + log_filename: Where to the put the log file. + prefix: The logging domain prefix that should be sent to the log. + """ + log_file_handler = logging.FileHandler(log_filename, delay=True) + log_file_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '[%(asctime)s] - %(name)s - %(levelname)7s -%(pageno)s %(message)s' + ) + log_file_handler.setFormatter(formatter) + log_file_handler.addFilter(PageNumberFilter()) + logging.getLogger(prefix).addHandler(log_file_handler) + return log_file_handler + + +def run_pipeline(options, *, plugin_manager, api=False): + # Any changes to options will not take effect for options that are already + # bound to function parameters in the pipeline. (For example + # options.input_file, options.pdf_renderer are already bound.) + if not options.jobs: + options.jobs = available_cpu_count() + if not plugin_manager: + plugin_manager = get_plugin_manager(options.plugins) + + work_folder = Path(mkdtemp(prefix="ocrmypdf.io.")) + debug_log_handler = None + if ( + (options.keep_temporary_files or options.verbose >= 1) + and not os.environ.get('PYTEST_CURRENT_TEST', '') + and not api + ): + # Debug log for command line interface only with verbose output + # See https://github.com/pytest-dev/pytest/issues/5502 for why we skip this + # when pytest is running + debug_log_handler = configure_debug_logging( + Path(work_folder) / "debug.log" + ) # pragma: no cover + + pikepdf_enable_mmap() + + executor = setup_executor(plugin_manager) + try: + check_requested_output_file(options) + start_input_file, original_filename = create_input_file(options, work_folder) + + # Triage image or pdf + origin_pdf = triage( + original_filename, start_input_file, work_folder / 'origin.pdf', options + ) + + # Gather pdfinfo and create context + pdfinfo = get_pdfinfo( + origin_pdf, + executor=executor, + detailed_analysis=options.redo_ocr, + progbar=options.progress_bar, + max_workers=options.jobs if not options.use_threads else 1, # To help debug + check_pages=options.pages, + ) + + context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager) + + # Validate options are okay for this pdf + validate_pdfinfo_options(context) + + # Execute the pipeline + exec_concurrent(context, executor) + + if options.output_file == '-': + log.info("Output sent to stdout") + elif ( + hasattr(options.output_file, 'writable') and options.output_file.writable() + ): + log.info("Output written to stream") + elif samefile(options.output_file, os.devnull): + pass # Say nothing when sending to dev null + else: + if options.output_type.startswith('pdfa'): + pdfa_info = file_claims_pdfa(options.output_file) + if pdfa_info['pass']: + log.info( + "Output file is a %s (as expected)", pdfa_info['conformance'] + ) + else: + log.warning( + "Output file is okay but is not PDF/A (seems to be %s)", + pdfa_info['conformance'], + ) + return ExitCode.pdfa_conversion_failed + if not check_pdf(options.output_file): + log.warning('Output file: The generated PDF is INVALID') + return ExitCode.invalid_output_pdf + report_output_file_size(options, start_input_file, options.output_file) + + except (KeyboardInterrupt if not api else NeverRaise) as e: + if options.verbose >= 1: + log.exception("KeyboardInterrupt") + else: + log.error("KeyboardInterrupt") + return ExitCode.ctrl_c + except (ExitCodeException if not api else NeverRaise) as e: + if str(e): + log.error("%s: %s", type(e).__name__, str(e)) + else: + log.error(type(e).__name__) + return e.exit_code + except (Exception if not api else NeverRaise) as e: # pylint: disable=broad-except + log.exception("An exception occurred while executing the pipeline") + return ExitCode.other_error + finally: + if debug_log_handler: + try: + debug_log_handler.close() + log.removeHandler(debug_log_handler) + except EnvironmentError as e: + print(e, file=sys.stderr) + cleanup_working_files(work_folder, options) + + return ExitCode.ok diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py new file mode 100644 index 00000000..ab48195e --- /dev/null +++ b/src/ocrmypdf/_validation.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +# © 2015-17 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import locale +import logging +import os +import sys +import unicodedata +from pathlib import Path +from shutil import copyfileobj +from typing import List, Set, Tuple, Union + +import pikepdf +import PIL + +from ocrmypdf._exec import jbig2enc, pngquant, unpaper +from ocrmypdf._unicodefun import verify_python3_env +from ocrmypdf.exceptions import ( + BadArgsError, + InputFileError, + MissingDependencyError, + OutputFileAccessError, +) +from ocrmypdf.helpers import ( + is_file_writable, + is_iterable_notstr, + monotonic, + safe_symlink, +) +from ocrmypdf.subprocess import check_external_program + +# ------------- +# External dependencies + +HOCR_OK_LANGS = frozenset(['eng', 'deu', 'spa', 'ita', 'por']) +DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony + +log = logging.getLogger(__name__) + + +# -------- +# Critical environment tests +verify_python3_env() + + +def check_platform(): + if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover + # 32-bit interpreter on Windows + log.error( + "You are running OCRmyPDF in a 32-bit (x86) Python interpreter." + "Please use a 64-bit (x86-64) version of Python." + ) + + +def check_options_languages(options, ocr_engine_languages): + if not options.languages: + options.languages = {DEFAULT_LANGUAGE} + system_lang = locale.getlocale()[0] + if system_lang and not system_lang.startswith('en'): + log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE) + if not ocr_engine_languages: + return + if not options.languages.issubset(ocr_engine_languages): + msg = ( + f"OCR engine does not have language data for the following " + "requested languages: \n" + ) + for lang in options.languages - ocr_engine_languages: + msg += lang + '\n' + raise MissingDependencyError(msg) + + +def check_options_output(options): + is_latin = options.languages.issubset(HOCR_OK_LANGS) + + if options.pdf_renderer.startswith('hocr') and not is_latin: + msg = ( + "The 'hocr' PDF renderer is known to cause problems with one " + "or more of the languages in your document. Use " + "--pdf-renderer auto (the default) to avoid this issue." + ) + log.warning(msg) + + lossless_reconstruction = False + if not any( + ( + options.deskew, + options.clean_final, + options.force_ocr, + options.remove_background, + ) + ): + lossless_reconstruction = True + options.lossless_reconstruction = lossless_reconstruction + + if not options.lossless_reconstruction and options.redo_ocr: + raise BadArgsError( + "--redo-ocr is not currently compatible with --deskew, " + "--clean-final, and --remove-background" + ) + + +def check_options_sidecar(options): + if options.sidecar == '\0': + if options.output_file == '-': + raise BadArgsError( + "--sidecar filename must be specified when output file is stdout." + ) + options.sidecar = options.output_file + '.txt' + if options.sidecar == options.input_file or options.sidecar == options.output_file: + raise BadArgsError( + "--sidecar file must be different from the input and output files" + ) + + +def check_options_preprocessing(options): + if options.clean_final: + options.clean = True + if options.unpaper_args and not options.clean: + raise BadArgsError("--clean is required for --unpaper-args") + if options.clean: + check_external_program( + program='unpaper', + package='unpaper', + version_checker=unpaper.version, + need_version='6.1', + required_for=['--clean, --clean-final'], + ) + try: + if options.unpaper_args: + options.unpaper_args = unpaper.validate_custom_args( + options.unpaper_args + ) + except Exception as e: + raise BadArgsError("--unpaper-args: " + str(e)) from e + + +def _pages_from_ranges(ranges: str) -> Set[int]: + if is_iterable_notstr(ranges): + return set(ranges) + pages: List[int] = [] + page_groups = ranges.replace(' ', '').split(',') + for g in page_groups: + if not g: + continue + try: + start, end = g.split('-') + except ValueError: + pages.append(int(g) - 1) + else: + try: + new_pages = list(range(int(start) - 1, int(end))) + if not new_pages: + raise BadArgsError(f"invalid page subrange '{start}-{end}'") + pages.extend(new_pages) + except ValueError: + raise BadArgsError("invalid page range") from None + + if not pages: + raise BadArgsError( + f"The string of page ranges '{ranges}' did not contain any recognizable " + f"page ranges." + ) + + if not monotonic(pages): + log.warning( + "List of pages to process contains duplicate pages, or pages that are " + "out of order" + ) + if any(page < 0 for page in pages): + raise BadArgsError("pages refers to a page number less than 1") + + log.debug("OCRing only these pages: %s", pages) + return set(pages) + + +def check_options_ocr_behavior(options): + exclusive_options = sum( + [ + (1 if opt else 0) + for opt in (options.force_ocr, options.skip_text, options.redo_ocr) + ] + ) + if exclusive_options >= 2: + raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.") + if options.pages: + options.pages = _pages_from_ranges(options.pages) + + +def check_options_optimizing(options): + if options.optimize >= 2: + check_external_program( + program='pngquant', + package='pngquant', + version_checker=pngquant.version, + need_version='2.0.1', + required_for='--optimize {2,3}', + ) + + if options.optimize >= 2: + # Although we use JBIG2 for optimize=1, don't nag about it unless the + # user is asking for more optimization + check_external_program( + program='jbig2', + package='jbig2enc', + version_checker=jbig2enc.version, + need_version='0.28', + required_for='--optimize {2,3} | --jbig2-lossy', + recommended=True if not options.jbig2_lossy else False, + ) + + if options.optimize == 0 and any( + [options.jbig2_lossy, options.png_quality, options.jpeg_quality] + ): + log.warning( + "The arguments --jbig2-lossy, --png-quality, and --jpeg-quality " + "will be ignored because --optimize=0." + ) + + +def check_options_advanced(options): + if options.pdfa_image_compression != 'auto' and not options.output_type.startswith( + 'pdfa' + ): + 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): + docinfo = [options.title, options.author, options.keywords, options.subject] + for s in (m for m in docinfo if m): + for c in s: + if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000: + raise ValueError( + "One of the metadata strings contains " + "an unsupported Unicode character: '{}' (U+{})".format( + c, hex(ord(c))[2:].upper() + ) + ) + + +def check_options_pillow(options): + PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000) + if PIL.Image.MAX_IMAGE_PIXELS == 0: + PIL.Image.MAX_IMAGE_PIXELS = None + + +def _check_options(options, plugin_manager, ocr_engine_languages): + check_platform() + check_options_languages(options, ocr_engine_languages) + check_options_metadata(options) + check_options_output(options) + check_options_sidecar(options) + check_options_preprocessing(options) + check_options_ocr_behavior(options) + check_options_optimizing(options) + check_options_advanced(options) + check_options_pillow(options) + plugin_manager.hook.check_options(options=options) + + +def check_options(options, plugin_manager): + ocr_engine_languages = plugin_manager.hook.get_ocr_engine().languages(options) + _check_options(options, plugin_manager, ocr_engine_languages) + + +def check_closed_streams(options): # pragma: no cover + """Work around Python issue with multiprocessing forking on closed streams + + https://bugs.python.org/issue28326 + + Attempting to a fork/exec a new Python process when any of std{in,out,err} + are closed or not flushable for some reason may raise an exception. + Fix this by opening devnull if the handle seems to be closed. Do this + globally to avoid tracking places all places that fork. + + Seems to be specific to multiprocessing.Process not all Python process + forkers. + + The error actually occurs when the stream object is not flushable, + but replacing an open stream object that is not flushable with + /dev/null is a bad idea since it will create a silent failure. Replacing + a closed handle with /dev/null seems safe. + + """ + + if sys.version_info[0:3] >= (3, 6, 4): + return True # Issued fixed in Python 3.6.4+ + + if sys.stderr is None: + sys.stderr = open(os.devnull, 'w') + + if sys.stdin is None: + if options.input_file == '-': + log.error("Trying to read from stdin but stdin seems closed") + return False + sys.stdin = open(os.devnull, 'r') + + if sys.stdout is None: + if options.output_file == '-': + # Can't replace stdout if the user is piping + # If this case can even happen, it must be some kind of weird + # stream. + log.error( + "Output was set to stdout '-' but the stream attached to " + "stdout does not support the flush() system call. This " + "will fail." + ) + return False + sys.stdout = open(os.devnull, 'w') + + return True + + +def create_input_file(options, work_folder: Path) -> Tuple[Path, str]: + if options.input_file == '-': + # stdin + log.info('reading file from standard input') + target = work_folder / 'stdin' + with open(target, 'wb') as stream_buffer: + copyfileobj(sys.stdin.buffer, stream_buffer) + return target, "stdin" + elif hasattr(options.input_file, 'readable'): + if not options.input_file.readable(): + raise InputFileError("Input file stream is not readable") + log.info('reading file from input stream') + target = work_folder / 'stream' + with open(target, 'wb') as stream_buffer: + copyfileobj(options.input_file, stream_buffer) + return target, "stream" + else: + try: + target = work_folder / 'origin' + safe_symlink(options.input_file, target) + return target, os.fspath(options.input_file) + except FileNotFoundError: + msg = f"File not found - {options.input_file}" + if Path('/.dockerenv').exists(): # pragma: no cover + msg += ( + "\nDocker cannot your working directory unless you " + "explicitly share it with the Docker container and set up" + "permissions correctly.\n" + "You may find it easier to use stdin/stdout:" + "\n" + "\tdocker run -i --rm jbarlow83/ocrmypdf - - output.pdf\n" + ) + raise InputFileError(msg) + + +def check_requested_output_file(options): + if options.output_file == '-': + if sys.stdout.isatty(): + raise BadArgsError( + "Output was set to stdout '-' but it looks like stdout " + "is connected to a terminal. Please redirect stdout to a " + "file." + ) + elif hasattr(options.output_file, 'writable'): + if not options.output_file.writable(): + raise OutputFileAccessError("Output stream is not writable") + elif not is_file_writable(options.output_file): + raise OutputFileAccessError( + f"Output file location ({options.output_file}) is not a writable file." + ) + + +def report_output_file_size(options, input_file, output_file): + try: + output_size = Path(output_file).stat().st_size + input_size = Path(input_file).stat().st_size + except FileNotFoundError: + return # Outputting to stream or something + with pikepdf.open(output_file) as p: + # Overhead constants obtained by estimating amount of data added by OCR + # PDF/A conversion, and possible XMP metadata addition, with compression + FILE_OVERHEAD = 4000 + OCR_PER_PAGE_OVERHEAD = 3000 + reasonable_overhead = FILE_OVERHEAD + OCR_PER_PAGE_OVERHEAD * len(p.pages) + ratio = output_size / input_size + reasonable_ratio = output_size / (input_size + reasonable_overhead) + if reasonable_ratio < 1.35 or input_size < 25000: + return # Seems fine + + reasons = [] + image_preproc = { + 'deskew', + 'clean_final', + 'remove_background', + 'oversample', + 'force_ocr', + } + for arg in image_preproc: + if getattr(options, arg, False): + reasons.append( + f"The argument --{arg.replace('_', '-')} was issued, causing transcoding." + ) + + if options.optimize == 0: + reasons.append("Optimization was disabled.") + else: + image_optimizers = { + 'jbig2': jbig2enc.available(), + 'pngquant': pngquant.available(), + } + for name, available in image_optimizers.items(): + if not available: + reasons.append( + f"The optional dependency '{name}' was not found, so some image " + f"optimizations could not be attempted." + ) + if options.output_type.startswith('pdfa'): + reasons.append("PDF/A conversion was enabled. (Try `--output-type pdf`.)") + if options.plugins: + reasons.append("Plugins were used.") + + if reasons: + explanation = "Possible reasons for this include:\n" + '\n'.join(reasons) + "\n" + else: + explanation = "No reason for this increase is known. Please report this issue." + + log.warning( + f"The output file size is {ratio:.2f}× larger than the input file.\n" + f"{explanation}" + ) diff --git a/src/ocrmypdf/_version.py b/src/ocrmypdf/_version.py new file mode 100644 index 00000000..6751fede --- /dev/null +++ b/src/ocrmypdf/_version.py @@ -0,0 +1,13 @@ +# © 2017 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import pkg_resources + +PROGRAM_NAME = 'ocrmypdf' + +# Official PEP 396 +__version__ = pkg_resources.get_distribution('ocrmypdf').version diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py deleted file mode 100644 index 6b3c8cdd..00000000 --- a/src/ocrmypdf/_weave.py +++ /dev/null @@ -1,332 +0,0 @@ -# © 2018 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -from contextlib import suppress -from itertools import groupby -from pathlib import Path -import os - -import pikepdf - -from .exec import tesseract -from .helpers import flatten_groups, page_number - - -MAX_REPLACE_PAGES = int(os.environ.get('_OCRMYPDF_MAX_REPLACE_PAGES', 100)) - - -def _update_page_resources(*, page, font, font_key, procset): - """Update this page's fonts with a reference to the Glyphless font""" - - if '/Resources' not in page: - page['/Resources'] = pikepdf.Dictionary({}) - resources = page['/Resources'] - try: - fonts = resources['/Font'] - except KeyError: - fonts = pikepdf.Dictionary({}) - if font_key is not None and font_key not in fonts: - fonts[font_key] = font - resources['/Font'] = fonts - - # Reassign /ProcSet to one that just lists everything - ProcSet is - # obsolete and doesn't matter but recommended for old viewer support - resources['/ProcSet'] = procset - - -def strip_invisible_text(pdf, page, log): - stream = [] - in_text_obj = False - render_mode = 0 - text_objects = [] - - page.page_contents_coalesce() - for operands, operator in pikepdf.parse_content_stream(page, ''): - if not in_text_obj: - if operator == pikepdf.Operator('BT'): - in_text_obj = True - render_mode = 0 - text_objects.append((operands, operator)) - else: - stream.append((operands, operator)) - else: - if operator == pikepdf.Operator('Tr'): - render_mode = operands[0] - text_objects.append((operands, operator)) - if operator == pikepdf.Operator('ET'): - in_text_obj = False - if render_mode != 3: - stream.extend(text_objects) - text_objects.clear() - - def convert(op): - try: - return op.unparse() - except AttributeError: - return str(op).encode('ascii') - - lines = [] - - for operands, operator in stream: - if operator == pikepdf.Operator('INLINE IMAGE'): - iim = operands[0] - line = iim.unparse() - else: - line = b' '.join(convert(op) for op in operands) + b' ' + operator.unparse() - lines.append(line) - - content_stream = b'\n'.join(lines) - page.Contents = pikepdf.Stream(pdf, content_stream) - - -def _weave_layers_graft( - *, pdf_base, page_num, text, font, font_key, procset, rotation, strip_old_text, log -): - """Insert the text layer from text page 0 on to pdf_base at page_num""" - - log.debug("Grafting") - if Path(text).stat().st_size == 0: - return - - # This is a pointer indicating a specific page in the base file - pdf_text = pikepdf.open(text) - pdf_text_contents = pdf_text.pages[0].Contents.read_bytes() - - if not tesseract.has_textonly_pdf(): - # If we don't have textonly_pdf, edit the stream to delete the - # instruction to draw the image Tesseract generated, which we do not - # use. - stream = bytearray(pdf_text_contents) - pattern = b'/Im1 Do' - idx = stream.find(pattern) - stream[idx : (idx + len(pattern))] = b' ' * len(pattern) - pdf_text_contents = bytes(stream) - - base_page = pdf_base.pages.p(page_num) - - # The text page always will be oriented up by this stage but the original - # content may have a rotation applied. Wrap the text stream with a rotation - # so it will be oriented the same way as the rest of the page content. - # (Previous versions OCRmyPDF rotated the content layer to match the text.) - mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)] - wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1] - - mediabox = [float(base_page.MediaBox[v]) for v in range(4)] - wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1] - - translate = pikepdf.PdfMatrix().translated(-wt / 2, -ht / 2) - untranslate = pikepdf.PdfMatrix().translated(wp / 2, hp / 2) - # -rotation because the input is a clockwise angle and this formula - # uses CCW - rotation = -rotation % 360 - rotate = pikepdf.PdfMatrix().rotated(rotation) - - # Because of rounding of DPI, we might get a text layer that is not - # identically sized to the target page. Scale to adjust. Normally this - # is within 0.998. - if rotation in (90, 270): - wt, ht = ht, wt - scale_x = wp / wt - scale_y = hp / ht - - log.debug('%r', (scale_x, scale_y)) - scale = pikepdf.PdfMatrix().scaled(scale_x, scale_y) - - # Translate the text so it is centered at (0, 0), rotate it there, adjust - # for a size different between initial and text PDF, then untranslate - ctm = translate @ rotate @ scale @ untranslate - - pdf_text_contents = b'q %s cm\n' % ctm.encode() + pdf_text_contents + b'\nQ\n' - - new_text_layer = pikepdf.Stream(pdf_base, pdf_text_contents) - - if strip_old_text: - strip_invisible_text(pdf_base, base_page, log) - - base_page.page_contents_add(new_text_layer, prepend=True) - - _update_page_resources( - page=base_page, font=font, font_key=font_key, procset=procset - ) - pdf_text.close() - - -def _find_font(text, pdf_base): - """Copy a font from the filename text into pdf_base""" - - font, font_key = None, None - possible_font_names = ('/f-0-0', '/F1') - try: - with pikepdf.open(text) as pdf_text: - try: - pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {}) - except (AttributeError, IndexError, KeyError): - return None, None - for f in possible_font_names: - pdf_text_font = pdf_text_fonts.get(f, None) - if pdf_text_font is not None: - font_key = f - break - if pdf_text_font: - font = pdf_base.copy_foreign(pdf_text_font) - return font, font_key - except (FileNotFoundError, pikepdf.PdfError): - # PdfError occurs if a 0-length file is written e.g. due to OCR timeout - return None, None - - -def weave_layers(infiles, output_file, log, context): - """Apply text layer and/or image layer changes to baseline file - - This is where the magic happens. infiles will be the main PDF to modify, - and optional .text.pdf and .image-layer.pdf files, organized however ruffus - organizes them. - - From .text.pdf, we copy the content stream (which contains the Tesseract - OCR results), and rotate it into place. The first time we do this, we also - copy the GlyphlessFont, and then reference that font again. - - For .image-layer.pdf, we check if this is a "pointer" to the original file, - or a new file. If a new file, we replace the page and remember that we - replaced this page. - - Every 100 open files, we save intermediate results, to avoid any resource - limits, since pikepdf/qpdf need to keep a lot of open file handles in the - background. When objects are copied from one file to another qpdf, qpdf - doesn't actually copy the data until asked to write, so all the resources - it may need to remain available. - - For completeness, we set up a /ProcSet on every page, although it's - unlikely any PDF viewer cares about this anymore. - - """ - - def input_sorter(key): - try: - return page_number(key) - except ValueError: - return -1 - - flat_inputs = sorted(flatten_groups(infiles), key=input_sorter) - groups = groupby(flat_inputs, key=input_sorter) - - # Extract first item - _, basegroup = next(groups) - base = list(basegroup)[0] - path_base = Path(base).resolve() - pdf_base = pikepdf.open(path_base) - font, font_key, procset = None, None, None - pdfinfo = context.get_pdfinfo() - - procset = pdf_base.make_indirect( - pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]') - ) - - emplacements = 1 - interim_count = 0 - - # Iterate rest - for page_num, layers in groups: - layers = list(layers) - log.debug(page_num) - log.debug(layers) - - text = next((ii for ii in layers if ii.endswith('.text.pdf')), None) - image = next((ii for ii in layers if ii.endswith('.image-layer.pdf')), None) - - if text and not font: - font, font_key = _find_font(text, pdf_base) - - emplaced_page = False - content_rotation = pdfinfo[page_num - 1].rotation - - path_image = Path(image).resolve() if image else None - if path_image is not None and path_image != path_base: - # We are updating the old page with a rasterized PDF of the new - # page (without changing objgen, to preserve references) - log.debug("Emplacement update") - with pikepdf.open(image) as pdf_image: - emplacements += 1 - foreign_image_page = pdf_image.pages[0] - pdf_base.pages.append(foreign_image_page) - local_image_page = pdf_base.pages[-1] - pdf_base.pages[page_num - 1].emplace(local_image_page) - del pdf_base.pages[-1] - emplaced_page = True - - autorotate_correction = context.get_rotation(page_num - 1) - if emplaced_page: - content_rotation = autorotate_correction - text_rotation = autorotate_correction - text_misaligned = (text_rotation - content_rotation) % 360 - log.debug( - '%r', - [text_rotation, autorotate_correction, text_misaligned, content_rotation], - ) - - if text and font: - # Graft the text layer onto this page, whether new or old - strip_old = context.get_options().redo_ocr - _weave_layers_graft( - pdf_base=pdf_base, - page_num=page_num, - text=text, - font=font, - font_key=font_key, - rotation=text_misaligned, - procset=procset, - strip_old_text=strip_old, - log=log, - ) - - # Correct the rotation if applicable - pdf_base.pages[page_num - 1].Rotate = ( - content_rotation - autorotate_correction - ) % 360 - - if emplacements % MAX_REPLACE_PAGES == 0: - # Periodically save and reload the Pdf object. This will keep a - # lid on our memory usage for very large files. Attach the font to - # page 1 even if page 1 doesn't use it, so we have a way to get it - # back. - # TODO refactor this to outside the loop - page0 = pdf_base.pages[0] - _update_page_resources( - page=page0, font=font, font_key=font_key, procset=procset - ) - - # We cannot read and write the same file, that will corrupt it - # but we don't to keep more copies than we need to. Delete intermediates. - # {interim_count} is the opened file we were updateing - # {interim_count - 1} can be deleted - # {interim_count + 1} is the new file will produce and open - old_file = output_file + f'_working{interim_count - 1}.pdf' - if not context.get_options().keep_temporary_files: - with suppress(FileNotFoundError): - os.unlink(old_file) - - next_file = output_file + f'_working{interim_count + 1}.pdf' - pdf_base.save(next_file) - pdf_base.close() - - pdf_base = pikepdf.open(next_file) - procset = pdf_base.pages[0].Resources.ProcSet - font, font_key = None, None # Ensure we reacquire this information - interim_count += 1 - - pdf_base.save(output_file) - pdf_base.close() diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py new file mode 100644 index 00000000..65ab910b --- /dev/null +++ b/src/ocrmypdf/api.py @@ -0,0 +1,340 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import os +import sys +import threading +from enum import IntEnum +from io import IOBase +from pathlib import Path +from typing import AnyStr, BinaryIO, Iterable, Optional, Union +from warnings import warn + +from ocrmypdf._logging import ( # pylint: disable=unused-import + PageNumberFilter, + TqdmConsole, +) +from ocrmypdf._plugin_manager import get_plugin_manager +from ocrmypdf._sync import run_pipeline +from ocrmypdf._validation import check_options +from ocrmypdf.cli import ArgumentParser, get_parser +from ocrmypdf.helpers import is_iterable_notstr + +try: + import coloredlogs +except ModuleNotFoundError: + coloredlogs = None + + +StrPath = Union[os.PathLike, AnyStr] +PathOrIO = Union[BinaryIO, StrPath] + +_api_lock = threading.Lock() + + +class Verbosity(IntEnum): + """Verbosity level for configure_logging.""" + + quiet = -1 #: Suppress most messages + default = 0 #: Default level of logging + debug = 1 #: Output ocrmypdf debug messages + debug_all = 2 #: More detailed debugging from ocrmypdf and dependent modules + + +def configure_logging( + verbosity: Verbosity, + *, + progress_bar_friendly: bool = True, + manage_root_logger: bool = False, + plugin_manager=None, +): + """Set up logging. + + Before calling :func:`ocrmypdf.ocr()`, you can use this function to + configure logging if you want ocrmypdf's output to look like the ocrmypdf + command line interface. It will register log handlers, log filters, and + formatters, configure color logging to standard error, and adjust the log + levels of third party libraries. Details of this are fine-tuned and subject + to change. The ``verbosity`` argument is equivalent to the argument + ``--verbose`` and applies those settings. If you have a wrapper + script for ocrmypdf and you want it to be very similar to ocrmypdf, use this + function; if you are using ocrmypdf as part of an application that manages + its own logging, you probably do not want this function. + + If this function is not called, ocrmypdf will not configure logging, and it + is up to the caller of ``ocrmypdf.ocr()`` to set up logging as it wishes using + the Python standard library's logging module. If this function is called, + the caller may of course make further adjustments to logging. + + Regardless of whether this function is called, ocrmypdf will perform all of + its logging under the ``"ocrmypdf"`` logging namespace. In addition, + ocrmypdf imports pdfminer, which logs under ``"pdfminer"``. A library user + may wish to configure both; note that pdfminer is extremely chatty at the + log level ``logging.INFO``. + + This function does not set up the ``debug.log`` log file that the command + line interface does at certain verbosity levels. Applications should configure + their own debug logging. + + Args: + verbosity: Verbosity level. + progress_bar_friendly: If True (the default), install a custom log handler + that is compatible with progress bars and colored output. + manage_root_logger: Configure the process's root logger. + plugin_manager: The plugin manager, used for obtaining the custom log handler. + + Returns: + The toplevel logger for ocrmypdf (or the root logger, if we are managing it). + """ + + prefix = '' if manage_root_logger else 'ocrmypdf' + + log = logging.getLogger(prefix) + log.setLevel(logging.DEBUG) + + console = None + if plugin_manager and progress_bar_friendly: + console = plugin_manager.hook.get_logging_console() + + if not console: + console = logging.StreamHandler(stream=sys.stderr) + + if verbosity < 0: + console.setLevel(logging.ERROR) + elif verbosity >= 1: + console.setLevel(logging.DEBUG) + else: + console.setLevel(logging.INFO) + + console.addFilter(PageNumberFilter()) + + if verbosity >= 2: + fmt = '%(levelname)7s %(name)s -%(pageno)s %(message)s' + else: + fmt = '%(pageno)s%(message)s' + + use_colors = progress_bar_friendly + if not coloredlogs: + use_colors = False + if use_colors: + if os.name == 'nt': + use_colors = coloredlogs.enable_ansi_support() + if use_colors: + use_colors = coloredlogs.terminal_supports_colors() + if use_colors: + formatter = coloredlogs.ColoredFormatter(fmt=fmt) + else: + formatter = logging.Formatter(fmt=fmt) + + console.setFormatter(formatter) + log.addHandler(console) + + if verbosity <= 1: + pdfminer_log = logging.getLogger('pdfminer') + pdfminer_log.setLevel(logging.ERROR) + pil_log = logging.getLogger('PIL') + pil_log.setLevel(logging.INFO) + + if manage_root_logger: + logging.captureWarnings(True) + + return log + + +def create_options( + *, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs +): + cmdline = [] + deferred = [] + + for arg, val in kwargs.items(): + if val is None: + continue + + # These arguments with special handling for which we bypass + # argparse + if arg in {'progress_bar', 'plugins'}: + deferred.append((arg, val)) + continue + + cmd_style_arg = arg.replace('_', '-') + + # Booleans are special: add only if True, omit for False + if isinstance(val, bool): + if val: + cmdline.append(f"--{cmd_style_arg}") + continue + + if is_iterable_notstr(val): + for elem in val: + cmdline.append(f"--{cmd_style_arg}") + cmdline.append(elem) + continue + + # We have a parameter + cmdline.append(f"--{cmd_style_arg}") + if isinstance(val, (int, float)): + cmdline.append(str(val)) + elif isinstance(val, str): + cmdline.append(val) + elif isinstance(val, Path): + cmdline.append(str(val)) + else: + raise TypeError(f"{arg}: {val} ({type(val)})") + + if isinstance(input_file, (BinaryIO, IOBase)): + cmdline.append('stream://input_file') + else: + cmdline.append(os.fspath(input_file)) + if isinstance(output_file, (BinaryIO, IOBase)): + cmdline.append('stream://output_file') + else: + cmdline.append(os.fspath(output_file)) + + parser._api_mode = True + options = parser.parse_args(cmdline) + for keyword, val in deferred: + setattr(options, keyword, val) + + if options.input_file == 'stream://input_file': + options.input_file = input_file + if options.output_file == 'stream://output_file': + options.output_file = output_file + + return options + + +def ocr( # pylint: disable=unused-argument + input_file: PathOrIO, + output_file: PathOrIO, + *, + language: Iterable[str] = None, + image_dpi: int = None, + output_type=None, + sidecar: Optional[StrPath] = None, + jobs: int = None, + use_threads: bool = None, + title: str = None, + author: str = None, + subject: str = None, + keywords: str = None, + rotate_pages: bool = None, + remove_background: bool = None, + deskew: bool = None, + clean: bool = None, + clean_final: bool = None, + unpaper_args: str = None, + oversample: int = None, + remove_vectors: bool = None, + threshold: bool = None, + force_ocr: bool = None, + skip_text: bool = None, + redo_ocr: bool = None, + skip_big: float = None, + optimize: int = None, + jpg_quality: int = None, + png_quality: int = None, + jbig2_lossy: bool = None, + jbig2_page_group_size: int = None, + pages: str = None, + max_image_mpixels: float = None, + tesseract_config: Iterable[str] = None, + tesseract_pagesegmode: int = None, + tesseract_oem: int = None, + pdf_renderer=None, + tesseract_timeout: float = None, + rotate_pages_threshold: float = None, + pdfa_image_compression=None, + user_words: os.PathLike = None, + user_patterns: os.PathLike = None, + fast_web_view: float = None, + plugins: Iterable[StrPath] = None, + plugin_manager=None, + keep_temporary_files: bool = None, + progress_bar: bool = None, + **kwargs, +): + """Run OCRmyPDF on one PDF or image. + + For most arguments, see documentation for the equivalent command line parameter. + A few specific arguments are discussed here: + + Args: + use_threads: Use worker threads instead of processes. This reduces + performance but may make debugging easier since it is easier to set + breakpoints. + input_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is + interpreted as file system path to the input file. If the object + appears to be a readable stream (with methods such as ``.read()`` + and ``.seek()``), the object will be read in its entirety and saved to + a temporary file. If ``input_file`` is ``"-"``, standard input will be + read. + output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is + interpreted as file system path to the output file. If the object + appears to be a writable stream (with methods such as ``.write()`` and + ``.seek()``), the output will be written to this stream. If + ``output_file`` is ``"-"``, the output will be written to ``sys.stdout`` + (provided that standard output does not seem to be a terminal device). + When a stream is used as output, whether via a writable object or + ``"-"``, some final validation steps are not performed (we do not read + back the stream after it is written). + Raises: + ocrmypdf.PdfMergeFailedError: If the input PDF is malformed, preventing merging + with the OCR layer. + ocrmypdf.MissingDependencyError: If a required dependency program is missing or + was not found on PATH. + ocrmypdf.UnsupportedImageFormatError: If the input file type was an image that + could not be read, or some other file type that is not a PDF. + ocrmypdf.DpiError: If the input file is an image, but the resolution of the + image is not credible (allowing it to proceed would cause poor OCR). + ocrmypdf.OutputFileAccessError: If an attempt to write to the intended output + file failed. + ocrmypdf.PriorOcrFoundError: If the input PDF seems to have OCR or digital + text already, and settings did not tell us to proceed. + ocrmypdf.InputFileError: Any other problem with the input file. + ocrmypdf.SubprocessOutputError: Any error related to executing a subprocess. + ocrmypdf.EncryptedPdfERror: If the input PDF is encrypted (password protected). + OCRmyPDF does not remove passwords. + ocrmypdf.TesseractConfigError: If Tesseract reported its configuration was not + valid. + + Returns: + :class:`ocrmypdf.ExitCode` + """ + if plugins and plugin_manager: + raise ValueError("plugins= and plugin_manager are mutually exclusive") + + if not plugins: + plugins = [] + elif isinstance(plugins, (str, Path)): + plugins = [plugins] + else: + plugins = list(plugins) + + # No new variable names should be assigned until these two steps are run + create_options_kwargs = {k: v for k, v in locals().items() if k != 'kwargs'} + create_options_kwargs.update(kwargs) + + parser = get_parser() + create_options_kwargs['parser'] = parser + + with _api_lock: + # We can't allow multiple ocrmypdf.ocr() threads to run in parallel, because + # they might install different plugins, and generally speaking we have areas + # of code that use global state. + + if not plugin_manager: + plugin_manager = get_plugin_manager(plugins) + plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member + + if 'verbose' in kwargs: + warn("ocrmypdf.ocr(verbose=) is ignored. Use ocrmypdf.configure_logging().") + + options = create_options(**create_options_kwargs) + check_options(options, plugin_manager) + return run_pipeline(options=options, plugin_manager=plugin_manager, api=True) diff --git a/src/ocrmypdf/builtin_plugins/__init__.py b/src/ocrmypdf/builtin_plugins/__init__.py new file mode 100644 index 00000000..05d8c70e --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/__init__.py @@ -0,0 +1,9 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This file exists only mark builtin_plugins as a package. +# The plugin manager will not load it, so anything defined here may not be +# processed as a module. diff --git a/src/ocrmypdf/builtin_plugins/concurrency.py b/src/ocrmypdf/builtin_plugins/concurrency.py new file mode 100644 index 00000000..797087ae --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/concurrency.py @@ -0,0 +1,172 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import logging.handlers +import multiprocessing +import os +import queue +import signal +import sys +import threading +from contextlib import suppress +from multiprocessing import Pool as ProcessPool +from multiprocessing.pool import ThreadPool +from typing import Callable, Iterable, Union + +from tqdm import tqdm + +from ocrmypdf import Executor, hookimpl +from ocrmypdf._logging import TqdmConsole +from ocrmypdf.exceptions import InputFileError +from ocrmypdf.helpers import remove_all_log_handlers + +Queue = Union[multiprocessing.Queue, queue.Queue] + + +def log_listener(q: Queue): + """Listen to the worker processes and forward the messages to logging + + For simplicity this is a thread rather than a process. Only one process + should actually write to sys.stderr or whatever we're using, so if this is + made into a process the main application needs to be directed to it. + + See https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes + """ + + while True: + try: + record = q.get() + if record is None: + break + logger = logging.getLogger(record.name) + logger.handle(record) + except Exception: # pylint: disable=broad-except + import traceback # pylint: disable=import-outside-toplevel + + print("Logging problem", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + + +def process_sigbus(*args): + raise InputFileError("A worker process lost access to an input file") + + +def process_init(q: Queue, user_init: Callable[[], None], loglevel): + """Initialize a process pool worker""" + + # Ignore SIGINT (our parent process will kill us gracefully) + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Install SIGBUS handler (so our parent process can abort somewhat gracefully) + with suppress(AttributeError): # Windows and Cygwin do not have SIGBUS + # Windows and Cygwin do not have pthread_sigmask or SIGBUS + signal.signal(signal.SIGBUS, process_sigbus) + + # Remove any log handlers that belong to the parent process + root = logging.getLogger() + remove_all_log_handlers(root) + + # Set up our single log handler to forward messages to the parent + root.setLevel(loglevel) + root.addHandler(logging.handlers.QueueHandler(q)) + + user_init() + return + + +def thread_init(_queue: Queue, user_init: Callable[[], None], _loglevel): + # As a thread, block SIGBUS so the main thread deals with it... + with suppress(AttributeError): + signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS}) + + user_init() + return + + +class StandardExecutor(Executor): + def _execute( + self, + *, + use_threads: bool, + max_workers: int, + tqdm_kwargs: dict, + worker_initializer: Callable, + task: Callable, + task_arguments: Iterable, + task_finished: Callable, + ): + if use_threads: + log_queue = queue.Queue(-1) + pool_class = ThreadPool + initializer = thread_init + else: + log_queue = multiprocessing.Queue(-1) + pool_class = ProcessPool + initializer = process_init + + # 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, + # so that it does not get forked into the workers. + listener = threading.Thread(target=log_listener, args=(log_queue,)) + listener.start() + + with self.pbar_class(**tqdm_kwargs) as pbar: + pool = pool_class( + processes=max_workers, + initializer=initializer, + initargs=(log_queue, worker_initializer, logging.getLogger("").level), + ) + try: + results = pool.imap_unordered(task, task_arguments) + for result in results: + if task_finished: + task_finished(result, pbar) + else: + pbar.update() + except KeyboardInterrupt: + # Terminate pool so we exit instantly + pool.terminate() + # Don't try listener.join() here, will deadlock + raise + except Exception: + if not os.environ.get("PYTEST_CURRENT_TEST", ""): + # Unless inside pytest, exit immediately because no one wants + # to wait for child processes to finalize results that will be + # thrown away. Inside pytest, we want child processes to exit + # cleanly so that they output an error messages or coverage data + # we need from them. + pool.terminate() + raise + finally: + # Terminate log listener + log_queue.put_nowait(None) + pool.close() + pool.join() + + listener.join() + + +@hookimpl +def get_executor(progressbar_class): + return StandardExecutor(pbar_class=progressbar_class) + + +@hookimpl +def get_progressbar_class(): + return tqdm + + +@hookimpl +def get_logging_console(): + return logging.StreamHandler(stream=TqdmConsole(sys.stderr)) diff --git a/src/ocrmypdf/builtin_plugins/default_filters.py b/src/ocrmypdf/builtin_plugins/default_filters.py new file mode 100644 index 00000000..da3f3aae --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/default_filters.py @@ -0,0 +1,14 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from ocrmypdf import hookimpl + + +@hookimpl +def filter_pdf_page( + page, image_filename, output_pdf +): # pylint: disable=unused-argument + return output_pdf diff --git a/src/ocrmypdf/builtin_plugins/ghostscript.py b/src/ocrmypdf/builtin_plugins/ghostscript.py new file mode 100644 index 00000000..d1c6a628 --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/ghostscript.py @@ -0,0 +1,99 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging + +from ocrmypdf import hookimpl +from ocrmypdf._exec import ghostscript +from ocrmypdf._validation import HOCR_OK_LANGS +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.subprocess import check_external_program + +log = logging.getLogger(__name__) + + +@hookimpl +def check_options(options): + gs_version = ghostscript.version() + check_external_program( + program='gs', + package='ghostscript', + version_checker=gs_version, + need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports + ) + if gs_version in ('9.24', '9.51'): + raise MissingDependencyError( + f"Ghostscript {gs_version} contains serious regressions and is not " + "supported. Please upgrade to a newer version, or downgrade to the " + "previous version." + ) + + # We have these constraints to check for. + # 1. Ghostscript < 9.20 mangles multibyte Unicode + # 2. hocr doesn't work on non-Latin languages (so don't select it) + is_latin = options.languages.issubset(HOCR_OK_LANGS) + if gs_version < '9.20' and options.output_type != 'pdf' and not is_latin: + # https://bugs.ghostscript.com/show_bug.cgi?id=696874 + # Ghostscript < 9.20 fails to encode multibyte characters properly + log.warning( + f"The installed version of Ghostscript ({gs_version}) does not work " + "correctly with the OCR languages you specified. Use --output-type pdf or " + "upgrade to Ghostscript 9.20 or later to avoid this issue." + ) + + if options.output_type == 'pdfa': + options.output_type = 'pdfa-2' + + if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19': + raise MissingDependencyError( + "--output-type pdfa-3 requires Ghostscript 9.19 or later" + ) + + +@hookimpl +def rasterize_pdf_page( + input_file, + output_file, + raster_device, + raster_dpi, + pageno, + page_dpi, + rotation, + filter_vector, +): + ghostscript.rasterize_pdf( + input_file, + output_file, + raster_device=raster_device, + raster_dpi=raster_dpi, + pageno=pageno, + page_dpi=page_dpi, + rotation=rotation, + filter_vector=filter_vector, + ) + return output_file + + +@hookimpl +def generate_pdfa( + pdf_pages, + pdfmark, + output_file, + compression, + pdf_version, + pdfa_part, + progressbar_class, +): + ghostscript.generate_pdfa( + pdf_pages=[*pdf_pages, pdfmark], + output_file=output_file, + compression=compression, + pdf_version=pdf_version, + pdfa_part=pdfa_part, + progressbar_class=progressbar_class, + ) + return output_file diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py new file mode 100644 index 00000000..a6973b01 --- /dev/null +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -0,0 +1,179 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import os + +from ocrmypdf import hookimpl +from ocrmypdf._exec import tesseract +from ocrmypdf.cli import numeric +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.helpers import clamp +from ocrmypdf.pluginspec import OcrEngine +from ocrmypdf.subprocess import check_external_program + +log = logging.getLogger(__name__) + + +@hookimpl +def add_options(parser): + tess = parser.add_argument_group("Tesseract", "Advanced control of Tesseract OCR") + tess.add_argument( + '--tesseract-config', + action='append', + metavar='CFG', + default=[], + help="Additional Tesseract configuration files -- see documentation", + ) + tess.add_argument( + '--tesseract-pagesegmode', + action='store', + type=int, + metavar='PSM', + choices=range(0, 14), + help="Set Tesseract page segmentation mode (see tesseract --help)", + ) + tess.add_argument( + '--tesseract-oem', + action='store', + type=int, + metavar='MODE', + choices=range(0, 4), + help=( + "Set Tesseract 4.0 OCR engine mode: " + "0 - original Tesseract only; " + "1 - neural nets LSTM only; " + "2 - Tesseract + LSTM; " + "3 - default." + ), + ) + tess.add_argument( + '--tesseract-timeout', + default=180.0, + type=numeric(float, 0), + metavar='SECONDS', + help='Give up on OCR after the timeout, but copy the preprocessed page ' + 'into the final output', + ) + tess.add_argument( + '--user-words', + metavar='FILE', + help="Specify the location of the Tesseract user words file. This is a " + "list of words Tesseract should consider while performing OCR in " + "addition to its standard language dictionaries. This can improve " + "OCR quality especially for specialized and technical documents.", + ) + tess.add_argument( + '--user-patterns', + metavar='FILE', + help="Specify the location of the Tesseract user patterns file.", + ) + + +@hookimpl +def check_options(options): + check_external_program( + program='tesseract', + package={'linux': 'tesseract-ocr'}, + version_checker=tesseract.version, + need_version='4.0.0-beta.1', # using backport for Travis CI + version_parser=tesseract.TesseractVersion, + ) + + # Decide on what renderer to use + if options.pdf_renderer == 'auto': + options.pdf_renderer = 'sandwich' + + if not tesseract.has_user_words() and (options.user_words or options.user_patterns): + log.warning( + "Tesseract 4.0 ignores --user-words and --user-patterns, so these " + "arguments have no effect." + ) + if options.tesseract_pagesegmode in (0, 2): + log.warning( + "The --tesseract-pagesegmode argument you select will disable OCR. " + "This may cause processing to fail." + ) + + +@hookimpl +def validate(pdfinfo, options): + # Tesseract 4.x can be multithreaded, and we also run multiple workers. We want + # to manage how many threads it uses to avoid creating total threads than cores. + # Performance testing shows we're better off + # parallelizing ocrmypdf and forcing Tesseract to be single threaded, which we + # get by setting the envvar OMP_THREAD_LIMIT to 1. But if the page count of the + # input file is small, then we allow Tesseract to use threads, subject to the + # constraint: (ocrmypdf workers) * (tesseract threads) <= max_workers. + # As of Tesseract 4.1, 3 threads is the most effective on a 4 core/8 thread system. + if not os.environ.get('OMP_THREAD_LIMIT', '').isnumeric(): + tess_threads = clamp(options.jobs // len(pdfinfo), 1, 3) + os.environ['OMP_THREAD_LIMIT'] = str(tess_threads) + else: + tess_threads = int(os.environ['OMP_THREAD_LIMIT']) + log.debug("Using Tesseract OpenMP thread limit %d", tess_threads) + + +class TesseractOcrEngine(OcrEngine): + @staticmethod + def version(): + return tesseract.version() + + @staticmethod + def creator_tag(options): + tag = '-PDF' if options.pdf_renderer == 'sandwich' else '' + return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}" + + def __str__(self): + return f"Tesseract OCR {TesseractOcrEngine.version()}" + + @staticmethod + def languages(options): + return tesseract.get_languages() + + @staticmethod + def get_orientation(input_file, options): + return tesseract.get_orientation( + input_file, + engine_mode=options.tesseract_oem, + timeout=options.tesseract_timeout, + ) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + tesseract.generate_hocr( + input_file=input_file, + output_hocr=output_hocr, + output_text=output_text, + languages=options.languages, + engine_mode=options.tesseract_oem, + tessconfig=options.tesseract_config, + timeout=options.tesseract_timeout, + pagesegmode=options.tesseract_pagesegmode, + user_words=options.user_words, + user_patterns=options.user_patterns, + ) + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + tesseract.generate_pdf( + input_file=input_file, + output_pdf=output_pdf, + output_text=output_text, + languages=options.languages, + engine_mode=options.tesseract_oem, + tessconfig=options.tesseract_config, + timeout=options.tesseract_timeout, + pagesegmode=options.tesseract_pagesegmode, + user_words=options.user_words, + user_patterns=options.user_patterns, + ) + + +@hookimpl +def get_ocr_engine(): + return TesseractOcrEngine() diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py new file mode 100644 index 00000000..204b7b31 --- /dev/null +++ b/src/ocrmypdf/cli.py @@ -0,0 +1,486 @@ +# © 2015-19 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import argparse +from typing import Optional, Type, TypeVar + +from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME +from ocrmypdf._version import __version__ as _VERSION + +T = TypeVar('T') + + +def numeric(basetype: Type[T], min_: Optional[T] = None, max_: Optional[T] = None): + """Validator for numeric params""" + min_ = basetype(min_) if min_ is not None else None + max_ = basetype(max_) if max_ is not None else None + + def _numeric(string): + value = basetype(string) + if (min_ is not None and value < min_) or (max_ is not None and value > max_): + msg = "%r not in valid range %r" % (string, (min_, max_)) + raise argparse.ArgumentTypeError(msg) + return value + + _numeric.__name__ = basetype.__name__ + return _numeric + + +class ArgumentParser(argparse.ArgumentParser): + """Override parser's default behavior of calling sys.exit() + + https://stackoverflow.com/questions/5943249/python-argparse-and-controlling-overriding-the-exit-status-code + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._api_mode = False + + def error(self, message): + if not self._api_mode: + super().error(message) + return + raise ValueError(message) + + +class LanguageSetAction(argparse.Action): + def __init__(self, option_strings, dest, default=None, **kwargs): + if default is None: + default = set() + super().__init__(option_strings, dest, default=default, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + dest = getattr(namespace, self.dest) + if '+' in values: + dest.update(lang for lang in values.split('+')) + else: + dest.add(values) + + +def get_parser(): + parser = ArgumentParser( + prog=_PROGRAM_NAME, + allow_abbrev=True, + fromfile_prefix_chars='@', + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""\ +Generates a searchable PDF or PDF/A from a regular PDF. + +OCRmyPDF rasterizes each page of the input PDF, optionally corrects page +rotation and performs image processing, runs the Tesseract OCR engine on the +image, and then creates a PDF from the OCR information. +""", + epilog="""\ +OCRmyPDF attempts to keep the output file at about the same size. If a file +contains losslessly compressed images, and images in the output file will be +losslessly compressed as well. + +PDF is a page description file that attempts to preserve a layout exactly. +A PDF can contain vector objects (such as text or lines) and raster objects +(images). A page might have multiple images. OCRmyPDF is prepared to deal +with the wide variety of PDFs that exist in the wild. + +When a PDF page contains text, OCRmyPDF assumes that the page has already +been OCRed or is a "born digital" page that should not be OCRed. The default +behavior is to exit in this case without producing a file. You can use the +option --skip-text to ignore pages with text, or --force-ocr to rasterize +all objects on the page and produce an image-only PDF as output. + + ocrmypdf --skip-text file_with_some_text_pages.pdf output.pdf + + ocrmypdf --force-ocr word_document.pdf output.pdf + +If you are concerned about long-term archiving of PDFs, use the default option +--output-type pdfa which converts the PDF to a standardized PDF/A-2b. This +removes some features from the PDF such as Javascript or forms. If you want to +minimize the number of changes made to your PDF, use --output-type pdf. + +If OCRmyPDF is given an image file as input, it will attempt to convert the +image to a PDF before processing. For more control over the conversion of +images to PDF, use the Python package img2pdf or other image to PDF software. + +For example, this command uses img2pdf to convert all .png files beginning +with the 'page' prefix to a PDF, fitting each image on A4-sized paper, and +sending the result to OCRmyPDF through a pipe. + + img2pdf --pagesize A4 page*.png | ocrmypdf - myfile.pdf + +Online documentation is located at: + https://ocrmypdf.readthedocs.io/en/latest/introduction.html + +""", + ) + + parser.add_argument( + 'input_file', + metavar="input_pdf_or_image", + help="PDF file containing the images to be OCRed (or '-' to read from " + "standard input)", + ) + parser.add_argument( + 'output_file', + metavar="output_pdf", + help="Output searchable PDF file (or '-' to write to standard output). " + "Existing files will be ovewritten. If same as input file, the " + "input file will be updated only if processing is successful.", + ) + parser.add_argument( + '-l', + '--language', + dest='languages', + action=LanguageSetAction, + help="Language(s) of the file to be OCRed (see tesseract --list-langs for " + "all language packs installed in your system). Use -l eng+deu for " + "multiple languages.", + ) + parser.add_argument( + '--image-dpi', + metavar='DPI', + type=int, + help="For input image instead of PDF, use this DPI instead of file's.", + ) + parser.add_argument( + '--output-type', + choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3'], + default='pdfa', + help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for " + "long term archiving (default, recommended) but may not suitable " + "for users who want their file altered as little as possible. 'pdfa' " + "also has problems with full Unicode text. 'pdf' attempts to " + "preserve file contents as much as possible. 'pdf-a1' creates a " + "PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a " + "PDF/A3-b file.", + ) + + # Use null string '\0' as sentinel to indicate the user supplied no argument, + # since that is the only invalid character for filepaths on all platforms + # bool('\0') is True in Python + parser.add_argument( + '--sidecar', + nargs='?', + const='\0', + default=None, + metavar='FILE', + help="Generate sidecar text files that contain the same text recognized " + "by Tesseract. This may be useful for building a OCR text database. " + "If FILE is omitted, the sidecar file be named {output_file}.txt; the next " + "argument must NOT be the name of the input PDF. " + "If FILE is set to '-', the sidecar is written to stdout (a " + "convenient way to preview OCR quality). The output file and sidecar " + "may not both use stdout at the same time.", + ) + + parser.add_argument( + '--version', + action='version', + version=_VERSION, + help="Print program version and exit", + ) + + jobcontrol = parser.add_argument_group("Job control options") + jobcontrol.add_argument( + '-j', + '--jobs', + metavar='N', + type=numeric(int, 0, 256), + help="Use up to N CPU cores simultaneously (default: use all).", + ) + jobcontrol.add_argument( + '-q', '--quiet', action='store_true', help="Suppress INFO messages" + ) + jobcontrol.add_argument( + '-v', + '--verbose', + type=numeric(int, 0, 2), + default=0, + const=1, + nargs='?', + help="Print more verbose messages for each additional verbose level. Use " + "`-v 1` typically for much more detailed logging. Higher numbers " + "are probably only useful in debugging.", + ) + jobcontrol.add_argument( + '--no-progress-bar', + action='store_false', + dest='progress_bar', + help=argparse.SUPPRESS, + ) + jobcontrol.add_argument( + '--use-threads', action='store_true', help=argparse.SUPPRESS + ) + + metadata = parser.add_argument_group( + "Metadata options", + "Set output PDF/A metadata (default: copy input document's metadata)", + ) + metadata.add_argument( + '--title', type=str, help="Set document title (place multiple words in quotes)" + ) + metadata.add_argument('--author', type=str, help="Set document author") + metadata.add_argument( + '--subject', type=str, help="Set document subject description" + ) + metadata.add_argument('--keywords', type=str, help="Set document keywords") + + preprocessing = parser.add_argument_group( + "Image preprocessing options", + "Options to improve the quality of the final PDF and OCR", + ) + preprocessing.add_argument( + '-r', + '--rotate-pages', + action='store_true', + help="Automatically rotate pages based on detected text orientation", + ) + preprocessing.add_argument( + '--remove-background', + action='store_true', + help="Attempt to remove background from gray or color pages, setting it " + "to white ", + ) + preprocessing.add_argument( + '-d', + '--deskew', + action='store_true', + help="Deskew each page before performing OCR", + ) + preprocessing.add_argument( + '-c', + '--clean', + action='store_true', + help="Clean pages from scanning artifacts before performing OCR, and send " + "the cleaned page to OCR, but do not include the cleaned page in " + "the output", + ) + preprocessing.add_argument( + '-i', + '--clean-final', + action='store_true', + help="Clean page as above, and incorporate the cleaned image in the final " + "PDF. Might remove desired content.", + ) + preprocessing.add_argument( + '--unpaper-args', + type=str, + default=None, + help="A quoted string of arguments to pass to unpaper. Requires --clean. " + "Example: --unpaper-args '--layout double'.", + ) + preprocessing.add_argument( + '--oversample', + metavar='DPI', + type=numeric(int, 0, 5000), + default=0, + help="Oversample images to at least the specified DPI, to improve OCR " + "results slightly", + ) + preprocessing.add_argument( + '--remove-vectors', + action='store_true', + help="EXPERIMENTAL. Mask out any vector objects in the PDF so that they " + "will not be included in OCR. This can eliminate false characters.", + ) + preprocessing.add_argument( + '--threshold', + action='store_true', + help=( + "EXPERIMENTAL. Threshold image to 1bpp before sending it to Tesseract " + "for OCR. Can improve OCR quality compared to Tesseract's thresholder." + ), + ) + + ocrsettings = parser.add_argument_group("OCR options", "Control how OCR is applied") + ocrsettings.add_argument( + '-f', + '--force-ocr', + action='store_true', + help="Rasterize any text or vector objects on each page, apply OCR, and " + "save the rastered output (this rewrites the PDF)", + ) + ocrsettings.add_argument( + '-s', + '--skip-text', + action='store_true', + help="Skip OCR on any pages that already contain text, but include the " + "page in final output; useful for PDFs that contain a mix of " + "images, text pages, and/or previously OCRed pages", + ) + ocrsettings.add_argument( + '--redo-ocr', + action='store_true', + help="Attempt to detect and remove the hidden OCR layer from files that " + "were previously OCRed with OCRmyPDF or another program. Apply OCR " + "to text found in raster images. Existing visible text objects will " + "not be changed. If there is no existing OCR, OCR will be added.", + ) + ocrsettings.add_argument( + '--skip-big', + type=numeric(float, 0, 5000), + metavar='MPixels', + help="Skip OCR on pages larger than the specified amount of megapixels, " + "but include skipped pages in final output", + ) + + optimizing = parser.add_argument_group( + "Optimization options", "Control how the PDF is optimized after OCR" + ) + optimizing.add_argument( + '-O', + '--optimize', + type=int, + choices=range(0, 4), + default=1, + help=( + "Control how PDF is optimized after processing:" + "0 - do not optimize; " + "1 - do safe, lossless optimizations (default); " + "2 - do some lossy optimizations; " + "3 - do aggressive lossy optimizations (including lossy JBIG2)" + ), + ) + optimizing.add_argument( + '--jpeg-quality', + type=numeric(int, 0, 100), + default=0, + metavar='Q', + help=( + "Adjust JPEG quality level for JPEG optimization. " + "100 is best quality and largest output size; " + "1 is lowest quality and smallest output; " + "0 uses the default." + ), + ) + optimizing.add_argument( + '--jpg-quality', + type=numeric(int, 0, 100), + default=0, + metavar='Q', + dest='jpeg_quality', + help=argparse.SUPPRESS, # Alias for --jpeg-quality + ) + optimizing.add_argument( + '--png-quality', + type=numeric(int, 0, 100), + default=0, + metavar='Q', + help=( + "Adjust PNG quality level to use when quantizing PNGs. " + "Values have same meaning as with --jpeg-quality" + ), + ) + optimizing.add_argument( + '--jbig2-lossy', + action='store_true', + help=( + "Enable JBIG2 lossy mode (better compression, not suitable for some " + "use cases - see documentation)." + ), + ) + optimizing.add_argument( + '--jbig2-page-group-size', + type=numeric(int, 1, 10000), + default=0, + metavar='N', + # Adjust number of pages to consider at once for JBIG2 compression + help=argparse.SUPPRESS, + ) + + advanced = parser.add_argument_group( + "Advanced", "Advanced options to control OCRmyPDF" + ) + advanced.add_argument( + '--pages', + type=str, + help=( + "Limit OCR to the specified pages (ranges or comma separated), " + "skipping others" + ), + ) + advanced.add_argument( + '--max-image-mpixels', + action='store', + type=numeric(float, 0), + metavar='MPixels', + help="Set maximum number of pixels to unpack before treating an image as a " + "decompression bomb", + default=128.0, + ) + advanced.add_argument( + '--pdf-renderer', + choices=['auto', 'hocr', 'sandwich', 'hocrdebug'], + default='auto', + help="Choose OCR PDF renderer - the default option is to let OCRmyPDF " + "choose. See documentation for discussion.", + ) + advanced.add_argument( + '--rotate-pages-threshold', + default=14.0, + type=numeric(float, 0, 1000), + metavar='CONFIDENCE', + help="Only rotate pages when confidence is above this value (arbitrary " + "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( + '--fast-web-view', + type=numeric(float, 0), + default=1.0, + metavar="MEGABYTES", + help="If the size of file is more than this threshold (in MB), then " + "linearize the PDF for fast web viewing. This allows the PDF to be " + "displayed before it is fully downloaded in web browsers, but increases " + "the space required slightly. By default we skip this for small files " + "which do not benefit. If the threshold is 0 it will be apply to all files. " + "Set the threshold very high to disable.", + ) + advanced.add_argument( + '--plugin', + dest='plugins', + action='append', + default=[], + help="Name of plugin to import. Argument may be issued multiple times to " + "import multiple plugins. Plugins may be specified as module names in " + "Python syntax, provided they are installed in the same Python (virtual) " + "environment as ocrmypdf; or you may give the path to the Python file that " + "contains the plugin. Plugins must conform to the specification in the " + "OCRmyPDF documentation.", + ) + + debugging = parser.add_argument_group( + "Debugging", "Arguments to help with troubleshooting and debugging" + ) + debugging.add_argument( + '-k', + '--keep-temporary-files', + action='store_true', + help="Keep temporary files (helpful for debugging)", + ) + return parser + + +plugins_only_parser = ArgumentParser( + prog=_PROGRAM_NAME, fromfile_prefix_chars='@', add_help=False, allow_abbrev=False +) +plugins_only_parser.add_argument( + '--plugin', + dest='plugins', + action='append', + default=[], + help="Name of plugin to import.", +) diff --git a/src/ocrmypdf/exceptions.py b/src/ocrmypdf/exceptions.py index a2df1963..5228b241 100644 --- a/src/ocrmypdf/exceptions.py +++ b/src/ocrmypdf/exceptions.py @@ -1,19 +1,8 @@ # © 2016 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. from enum import IntEnum diff --git a/src/ocrmypdf/exec/__init__.py b/src/ocrmypdf/exec/__init__.py deleted file mode 100644 index 31d3603f..00000000 --- a/src/ocrmypdf/exec/__init__.py +++ /dev/null @@ -1,173 +0,0 @@ -# © 2016 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -"""Wrappers to manage subprocess calls""" - -import os -import re -import sys -from subprocess import run, STDOUT, PIPE, CalledProcessError -from ..exceptions import MissingDependencyError, ExitCode -from collections.abc import Mapping - - -def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'): - "Get the version of the specified program" - args_prog = [program, version_arg] - try: - proc = run( - args_prog, - close_fds=True, - universal_newlines=True, - stdout=PIPE, - stderr=STDOUT, - check=True, - ) - output = proc.stdout - except FileNotFoundError as e: - raise MissingDependencyError( - f"Could not find program '{program}' on the PATH" - ) from e - except CalledProcessError as e: - if e.returncode != 0: - raise MissingDependencyError( - f"Ran program '{program}' but it exited with an error:\n{e.output}" - ) from e - raise MissingDependencyError( - f"Could not find program '{program}' on the PATH" - ) from e - try: - version = re.match(regex, output.strip()).group(1) - except AttributeError as e: - raise MissingDependencyError( - f"The program '{program}' did not report its version. " - f"Message was:\n{output}" - ) - - return version - - -missing_program = ''' -The program '{program}' could not be executed or was not found on your -system PATH. -''' - -missing_optional_program = ''' -The program '{program}' could not be executed or was not found on your -system PATH. This program is required when you use the -{required_for} arguments. You could try omitting these arguments, or install -the package. -''' - -missing_recommend_program = ''' -The program '{program}' could not be executed or was not found on your -system PATH. This program is recommended when using the {required_for} arguments, -but not required, so we will proceed. For best results, install the program. -''' - -old_version = ''' -OCRmyPDF requires '{program}' {need_version} or higher. Your system appears -to have {found_version}. Please update this program. -''' - -old_version_required_for = ''' -OCRmyPDF requires '{program}' {need_version} or higher when run with the -{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to -proceed. For best results, install the program. -''' - -osx_install_advice = ''' -If you have homebrew installed, try these command to install the missing -package: - brew install {package} -''' - -linux_install_advice = ''' -On systems with the aptitude package manager (Debian, Ubuntu), try these -commands: - sudo apt-get update - sudo apt-get install {package} - -On RPM-based systems (Red Hat, Fedora), search for instructions on -installing the RPM for {program}. -''' - - -def _get_platform(): - if sys.platform.startswith('freebsd'): - return 'freebsd' - elif sys.platform.startswith('linux'): - return 'linux' - return sys.platform - - -def _error_trailer(log, program, package, **kwargs): - if isinstance(package, Mapping): - package = package[_get_platform()] - - if _get_platform() == 'darwin': - log.info(osx_install_advice.format(**locals())) - elif _get_platform() == 'linux': - log.info(linux_install_advice.format(**locals())) - - -def _error_missing_program(log, program, package, required_for, recommended): - if required_for: - log.error(missing_optional_program.format(**locals())) - elif recommended: - log.info(missing_recommend_program.format(**locals())) - else: - log.error(missing_program.format(**locals())) - _error_trailer(**locals()) - - -def _error_old_version( - log, program, package, need_version, found_version, required_for -): - if required_for: - log.error(old_version_required_for.format(**locals())) - else: - log.error(old_version.format(**locals())) - _error_trailer(**locals()) - - -def check_external_program( - *, - log, - program, - package, - version_checker, - need_version, - required_for=None, - recommended=False, -): - try: - found_version = version_checker() - except (CalledProcessError, FileNotFoundError, MissingDependencyError): - _error_missing_program(log, program, package, required_for, recommended) - if not recommended: - sys.exit(ExitCode.missing_dependency) - return - - if found_version < need_version: - _error_old_version( - log, program, package, need_version, found_version, required_for - ) - if not recommended: - sys.exit(ExitCode.missing_dependency) - - log.debug(f'Found {program} {found_version}') diff --git a/src/ocrmypdf/exec/ghostscript.py b/src/ocrmypdf/exec/ghostscript.py deleted file mode 100644 index 70497092..00000000 --- a/src/ocrmypdf/exec/ghostscript.py +++ /dev/null @@ -1,291 +0,0 @@ -# © 2017 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import re -from functools import lru_cache -from os import fspath -from shutil import copy -from subprocess import PIPE, STDOUT, run -from tempfile import NamedTemporaryFile - -from PIL import Image - -from . import get_version -from ..exceptions import SubprocessOutputError - - -@lru_cache(maxsize=1) -def version(): - return get_version('gs') - - -def jpeg_passthrough_available(): - """Returns True if the installed version of Ghostscript supports JPEG passthru - - Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23 - it gained the ability to keep JPEGs unmodified. However, the 9.23 - implementation was buggy and would deletes the last two bytes of images in - some cases, as reported here. - https://bugs.ghostscript.com/show_bug.cgi?id=699216 - - The issue was fixed for 9.24, hence that is the first version we consider - the feature available. (However, we don't use 9.24 at all, so the first - version that allows JPEG passthrough is 9.25. - - """ - return version() >= '9.24' - - -def _gs_error_reported(stream): - return re.search(r'error', stream, flags=re.IGNORECASE) - - -def extract_text(input_file, pageno=1): - """Use the txtwrite device to get text layout information out - - For details on options of -dTextFormat see - https://www.ghostscript.com/doc/current/VectorDevices.htm#TXT - - Format is like - - - - - - :param pageno: number of page to extract, or all pages if None - :return: XML-ish text representation in bytes - """ - - if pageno is not None: - pages = ['-dFirstPage=%i' % pageno, '-dLastPage=%i' % pageno] - else: - pages = [] - - args_gs = ( - [ - 'gs', - '-dQUIET', - '-dSAFER', - '-dBATCH', - '-dNOPAUSE', - '-sDEVICE=txtwrite', - '-dTextFormat=0', - ] - + pages - + ['-o', '-', fspath(input_file)] - ) - - p = run(args_gs, stdout=PIPE, stderr=PIPE) - if p.returncode != 0: - raise SubprocessOutputError( - 'Ghostscript text extraction failed\n%s\n%s\n%s' - % (input_file, p.stdout.decode(), p.stderr.decode()) - ) - - return p.stdout - - -def rasterize_pdf( - input_file, - output_file, - xres, - yres, - raster_device, - log, - pageno=1, - page_dpi=None, - rotation=None, - filter_vector=False, -): - """Rasterize one page of a PDF at resolution (xres, yres) in canvas units. - - The image is sized to match the integer pixels dimensions implied by - (xres, yres) even if those numbers are noninteger. The image's DPI will - be overridden with the values in page_dpi. - - :param input_file: pathlike - :param output_file: pathlike - :param xres: resolution at which to rasterize page - :param yres: - :param raster_device: - :param log: - :param pageno: page number to rasterize (beginning at page 1) - :param page_dpi: resolution tuple (x, y) overriding output image DPI - :param rotation: 0, 90, 180, 270: clockwise angle to rotate page - :param filter_vector: if True, remove vector graphics objects - :return: - """ - res = round(xres, 6), round(yres, 6) - if not page_dpi: - page_dpi = res - - with NamedTemporaryFile(delete=True) as tmp: - args_gs = ( - [ - 'gs', - '-dQUIET', - '-dSAFER', - '-dBATCH', - '-dNOPAUSE', - f'-sDEVICE={raster_device}', - f'-dFirstPage={pageno}', - f'-dLastPage={pageno}', - f'-r{res[0]:f}x{res[1]:f}', - ] - + (['-dFILTERVECTOR'] if filter_vector else []) - + [ - '-o', - tmp.name, - '-dAutoRotatePages=/None', # Probably has no effect on raster - '-f', - fspath(input_file), - ] - ) - - log.debug(args_gs) - p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True) - if _gs_error_reported(p.stdout): - log.error(p.stdout) - else: - log.debug(p.stdout) - - if p.returncode != 0: - log.error('Ghostscript rasterizing failed') - raise SubprocessOutputError() - - tmp.seek(0) - with Image.open(tmp) as im: - if rotation is not None: - log.debug("Rotating output by %i", rotation) - # rotation is a clockwise angle and Image.ROTATE_* is - # counterclockwise so this cancels out the rotation - if rotation == 90: - im = im.transpose(Image.ROTATE_90) - elif rotation == 180: - im = im.transpose(Image.ROTATE_180) - elif rotation == 270: - im = im.transpose(Image.ROTATE_270) - if rotation % 180 == 90: - page_dpi = page_dpi[1], page_dpi[0] - im.save(fspath(output_file), dpi=page_dpi) - - -def generate_pdfa( - pdf_pages, - output_file, - compression, - log, - threads=1, - pdf_version='1.5', - pdfa_part='2', -): - """Generate a PDF/A. - - The pdf_pages, a list files, will be merged into output_file. One or more - PDF files may be merged. One of the files in this list must be a pdfmark - file that provides Ghostscript with details on how to perform the PDF/A - conversion. By default with we pick PDF/A-2b, but this works for 1 or 3. - - compression can be 'jpeg', 'lossless', or an empty string. In 'jpeg', - Ghostscript is instructed to convert color and grayscale images to DCT - (JPEG encoding). In 'lossless' Ghostscript is told to convert images to - Flate (lossless/PNG). If the parameter is omitted Ghostscript is left to - make its own decisions about how to encode images; it appears to use a - heuristic to decide how to encode images. As of Ghostscript 9.25, we - support passthrough JPEG which allows Ghostscript to avoid transcoding - images entirely. (The feature was added in 9.23 but broken, and the 9.24 - release of Ghostscript had regressions, so we don't support it until 9.25.) - """ - compression_args = [] - if compression == 'jpeg': - compression_args = [ - "-dAutoFilterColorImages=false", - "-dColorImageFilter=/DCTEncode", - "-dAutoFilterGrayImages=false", - "-dGrayImageFilter=/DCTEncode", - ] - elif compression == 'lossless': - compression_args = [ - "-dAutoFilterColorImages=false", - "-dColorImageFilter=/FlateEncode", - "-dAutoFilterGrayImages=false", - "-dGrayImageFilter=/FlateEncode", - ] - else: - compression_args = [ - "-dAutoFilterColorImages=true", - "-dAutoFilterGrayImages=true", - ] - - # Older versions of Ghostscript expect a leading slash in - # sColorConversionStrategy, newer ones should not have it. See Ghostscript - # git commit fe1c025d. - strategy = 'RGB' if version() >= '9.19' else '/RGB' - - if version() == '9.23': - # 9.23: new feature JPEG passthrough is broken in some cases, best to - # disable it always - # https://bugs.ghostscript.com/show_bug.cgi?id=699216 - compression_args.append('-dPassThroughJPEGImages=false') - - with NamedTemporaryFile(delete=True) as gs_pdf: - # nb no need to specify ProcessColorModel when ColorConversionStrategy - # is set; see: - # https://bugs.ghostscript.com/show_bug.cgi?id=699392 - args_gs = ( - [ - "gs", - "-dQUIET", - "-dBATCH", - "-dNOPAUSE", - "-dCompatibilityLevel=" + str(pdf_version), - "-sDEVICE=pdfwrite", - "-dAutoRotatePages=/None", - "-sColorConversionStrategy=" + strategy, - ] - + compression_args - + [ - "-dJPEGQ=95", - "-dPDFA=" + pdfa_part, - "-dPDFACompatibilityPolicy=1", - "-sOutputFile=" + gs_pdf.name, - ] - ) - args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs - log.debug(args_gs) - p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True) - - if _gs_error_reported(p.stdout): - log.error(p.stdout) - elif 'overprint mode not set' in p.stdout: - # Unless someone is going to print PDF/A documents on a - # magical sRGB printer I can't see the removal of overprinting - # being a problem.... - log.debug( - "Ghostscript had to remove PDF 'overprinting' from the " - "input file to complete PDF/A conversion. " - ) - else: - log.debug(p.stdout) - - if p.returncode == 0: - # Ghostscript does not change return code when it fails to create - # PDF/A - check PDF/A status elsewhere - copy(gs_pdf.name, fspath(output_file)) - else: - log.error('Ghostscript PDF/A rendering failed') - raise SubprocessOutputError() diff --git a/src/ocrmypdf/exec/pngquant.py b/src/ocrmypdf/exec/pngquant.py deleted file mode 100644 index 536dca1c..00000000 --- a/src/ocrmypdf/exec/pngquant.py +++ /dev/null @@ -1,70 +0,0 @@ -# © 2018 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -from functools import lru_cache -from subprocess import run -from tempfile import NamedTemporaryFile - -from PIL import Image - -from . import get_version -from ..exceptions import MissingDependencyError - - -@lru_cache(maxsize=1) -def version(): - return get_version('pngquant', regex=r'(\d+(\.\d+)*).*') - - -def available(): - try: - version() - except MissingDependencyError: - return False - return True - - -def quantize(input_file, output_file, quality_min, quality_max): - if input_file.endswith('.jpg'): - im = Image.open(input_file) - with NamedTemporaryFile(suffix='.png') as tmp: - im.save(tmp) - args = [ - 'pngquant', - '--force', - '--skip-if-larger', - '--output', - output_file, - '--quality', - f'{quality_min}-{quality_max}', - '--', - tmp.name, - ] - run(args) - else: - args = [ - 'pngquant', - '--force', - '--skip-if-larger', - '--output', - output_file, - '--quality', - f'{quality_min}-{quality_max}', - '--', - input_file, - ] - run(args) diff --git a/src/ocrmypdf/exec/qpdf.py b/src/ocrmypdf/exec/qpdf.py deleted file mode 100644 index 653cc4ff..00000000 --- a/src/ocrmypdf/exec/qpdf.py +++ /dev/null @@ -1,49 +0,0 @@ -# © 2017 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -from functools import lru_cache -from os import fspath -from subprocess import PIPE, STDOUT, CalledProcessError, run - -from . import get_version - - -@lru_cache(maxsize=1) -def version(): - return get_version('qpdf', regex=r'qpdf version (.+)') - - -def check(input_file, log=None): - args_qpdf = ['qpdf', '--check', fspath(input_file)] - - if log is None: - import logging as log - - try: - run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True, check=True) - except CalledProcessError as e: - if e.returncode == 2: - log.error("%s: not a valid PDF, and could not repair it.", input_file) - log.error("Details:") - log.error(e.output) - elif e.returncode == 3: - log.info("qpdf --check returned warnings:") - log.info(e.output) - else: - log.warning(e.output) - return False - return True diff --git a/src/ocrmypdf/exec/tesseract.py b/src/ocrmypdf/exec/tesseract.py deleted file mode 100644 index a81d591d..00000000 --- a/src/ocrmypdf/exec/tesseract.py +++ /dev/null @@ -1,361 +0,0 @@ -# © 2017 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import os -import shutil -import sys -from collections import namedtuple -from contextlib import suppress -from functools import lru_cache -from os import fspath -from subprocess import ( - PIPE, - STDOUT, - CalledProcessError, - TimeoutExpired, - check_output, - run, -) -from textwrap import dedent - -from . import get_version -from ..exceptions import MissingDependencyError, TesseractConfigError -from ..helpers import page_number - -OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence')) - -HOCR_TEMPLATE = """ - - - - - - - - - -
-
- - -""" - - -@lru_cache(maxsize=1) -def version(): - return get_version('tesseract', regex=r'tesseract\s(.+)') - - -def v4(): - "Is this Tesseract v4.0?" - return version() >= '4' - - -@lru_cache(maxsize=1) -def has_textonly_pdf(): - """Does Tesseract have textonly_pdf capability? - - Available in v4.00.00alpha since January 2017. Best to - parse the parameter list - """ - args_tess = ['tesseract', '--print-parameters', 'pdf'] - params = '' - try: - params = check_output(args_tess, universal_newlines=True, stderr=STDOUT) - except CalledProcessError as e: - print("Could not --print-parameters from tesseract", file=sys.stderr) - raise MissingDependencyError from e - if 'textonly_pdf' in params: - return True - return False - - -@lru_cache(maxsize=1) -def languages(): - def lang_error(output): - msg = dedent( - """Tesseract failed to report available languages. - Output from Tesseract: - ----------- - """ - ) - msg += output - print(msg, file=sys.stderr) - - args_tess = ['tesseract', '--list-langs'] - try: - proc = run( - args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True - ) - output = proc.stdout - except CalledProcessError as e: - lang_error(e.output) - raise MissingDependencyError from e - - header, *rest = output.splitlines() - if not header.startswith('List of available languages'): - lang_error(output) - raise MissingDependencyError - return set(lang.strip() for lang in rest) - - -def tess_base_args(langs, engine_mode): - args = ['tesseract'] - if langs: - args.extend(['-l', '+'.join(langs)]) - if engine_mode is not None and v4(): - args.extend(['--oem', str(engine_mode)]) - return args - - -def get_orientation(input_file, engine_mode, timeout: float, log): - args_tesseract = tess_base_args(['osd'], engine_mode) + [ - '--psm', - '0', - fspath(input_file), - 'stdout', - ] - - try: - stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout) - except TimeoutExpired: - return OrientationConfidence(angle=0, confidence=0.0) - except CalledProcessError as e: - tesseract_log_output(log, e.output, input_file) - if ( - b'Too few characters. Skipping this page' in e.output - or b'Image too large' in e.output - ): - return OrientationConfidence(0, 0) - raise e from e - else: - osd = {} - for line in stdout.decode().splitlines(): - line = line.strip() - parts = line.split(':', maxsplit=2) - if len(parts) == 2: - osd[parts[0].strip()] = parts[1].strip() - - angle = int(osd.get('Orientation in degrees', 0)) - oc = OrientationConfidence( - angle=angle, confidence=float(osd.get('Orientation confidence', 0)) - ) - return oc - - -def tesseract_log_output(log, stdout, input_file): - prefix = f"{(page_number(input_file)):4d}: [tesseract] " - - try: - text = stdout.decode() - except UnicodeDecodeError: - log.error( - prefix - + "command line output was not utf-8. " - + "This usually means Tesseract's language packs do not match " - "the installed version of Tesseract." - ) - text = stdout.decode('utf-8', 'backslashreplace') - - lines = text.splitlines() - for line in lines: - if line.startswith("Tesseract Open Source"): - continue - elif line.startswith("Warning in pixReadMem"): - continue - elif 'diacritics' in line: - log.warning(prefix + "lots of diacritics - possibly poor OCR") - elif line.startswith('OSD: Weak margin'): - log.warning(prefix + "unsure about page orientation") - elif 'Error in pixScanForForeground' in line: - pass # Appears to be spurious/problem with nonwhite borders - elif 'Error in boxClipToRectangle' in line: - pass # Always appears with pixScanForForeground message - elif 'parameter not found: ' in line.lower(): - log.error(prefix + line.strip()) - problem = line.split('found: ')[1] - raise TesseractConfigError(problem) - elif 'error' in line.lower() or 'exception' in line.lower(): - log.error(prefix + line.strip()) - elif 'warning' in line.lower(): - log.warning(prefix + line.strip()) - elif 'read_params_file' in line.lower(): - log.error(prefix + line.strip()) - else: - log.info(prefix + line.strip()) - - -def page_timedout(log, input_file, timeout): - if timeout == 0: - return - prefix = f"{(page_number(input_file)):4d}: [tesseract] " - log.warning(prefix + " took too long to OCR - skipping") - - -def _generate_null_hocr(output_hocr, output_sidecar, image): - """Produce a .hocr file that reports no text detected on a page that is - the same size as the input image.""" - from PIL import Image - - im = Image.open(image) - w, h = im.size - - with open(output_hocr, 'w', encoding="utf-8") as f: - f.write(HOCR_TEMPLATE.format(w, h)) - with open(output_sidecar, 'w', encoding='utf-8') as f: - f.write('[skipped page]') - - -def generate_hocr( - input_file, - output_files, - language: list, - engine_mode, - tessconfig: list, - timeout: float, - pagesegmode: int, - user_words, - user_patterns, - log, -): - - output_hocr = next(o for o in output_files if o.endswith('.hocr')) - output_sidecar = next(o for o in output_files if o.endswith('.txt')) - prefix = os.path.splitext(output_hocr)[0] - - args_tesseract = tess_base_args(language, engine_mode) - - if pagesegmode is not None: - args_tesseract.extend(['--psm', str(pagesegmode)]) - - if user_words: - args_tesseract.extend(['--user-words', user_words]) - - if user_patterns: - args_tesseract.extend(['--user-patterns', user_patterns]) - - # Reminder: test suite tesseract spoofers will break after any changes - # to the number of order parameters here - args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig) - try: - log.debug(args_tesseract) - stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout) - except TimeoutExpired: - # Generate a HOCR file with no recognized text if tesseract times out - # Temporary workaround to hocrTransform not being able to function if - # it does not have a valid hOCR file. - page_timedout(log, input_file, timeout) - _generate_null_hocr(output_hocr, output_sidecar, input_file) - except CalledProcessError as e: - tesseract_log_output(log, e.output, input_file) - if b'Image too large' in e.output: - _generate_null_hocr(output_hocr, output_sidecar, input_file) - return - - raise e from e - else: - tesseract_log_output(log, stdout, input_file) - # The sidecar text file will get the suffix .txt; rename it to - # whatever caller wants it named - if os.path.exists(prefix + '.txt'): - shutil.move(prefix + '.txt', output_sidecar) - - -def use_skip_page(text_only, skip_pdf, output_pdf, output_text): - with open(output_text, 'w') as f: - f.write('[skipped page]') - - if skip_pdf and not text_only: - # Substitute a "skipped page" - with suppress(FileNotFoundError): - os.remove(output_pdf) # In case it was partially created - os.symlink(skip_pdf, output_pdf) - return - - # Or normally, just write a 0 byte file to the output to indicate a skip - with open(output_pdf, 'wb') as out: - out.write(b'') - - -def generate_pdf( - *, - input_image, - skip_pdf=None, - output_pdf, - output_text, - language: list, - engine_mode, - text_only: bool, - tessconfig: list, - timeout: float, - pagesegmode: int, - user_words, - user_patterns, - log, -): - '''Use Tesseract to render a PDF. - - input_image -- image to analyze - skip_pdf -- if we time out, use this file as output - output_pdf -- file to generate - output_text -- OCR text file - language -- list of languages to consider - engine_mode -- engine mode argument for tess v4 - text_only -- enable tesseract text only mode? - tessconfig -- tesseract configuration - timeout -- timeout (seconds) - log -- logger object - ''' - - args_tesseract = tess_base_args(language, engine_mode) - - if pagesegmode is not None: - args_tesseract.extend(['--psm', str(pagesegmode)]) - - if text_only and has_textonly_pdf(): - args_tesseract.extend(['-c', 'textonly_pdf=1']) - - if user_words: - args_tesseract.extend(['--user-words', user_words]) - - if user_patterns: - args_tesseract.extend(['--user-patterns', user_patterns]) - - prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes - - # Reminder: test suite tesseract spoofers might break after any changes - # to the number of order parameters here - - args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig) - - try: - log.debug(args_tesseract) - stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout) - if os.path.exists(prefix + '.txt'): - shutil.move(prefix + '.txt', output_text) - except TimeoutExpired: - page_timedout(log, input_image, timeout) - use_skip_page(text_only, skip_pdf, output_pdf, output_text) - except CalledProcessError as e: - tesseract_log_output(log, e.output, input_image) - if b'Image too large' in e.output: - use_skip_page(text_only, skip_pdf, output_pdf, output_text) - return - raise e from e - else: - tesseract_log_output(log, stdout, input_image) diff --git a/src/ocrmypdf/exec/unpaper.py b/src/ocrmypdf/exec/unpaper.py deleted file mode 100644 index 40c79594..00000000 --- a/src/ocrmypdf/exec/unpaper.py +++ /dev/null @@ -1,129 +0,0 @@ -# © 2015 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -# unpaper documentation: -# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md - -import os -import shlex -import subprocess -import sys -from functools import lru_cache -from subprocess import PIPE, STDOUT, CalledProcessError -from tempfile import TemporaryDirectory - -from . import get_version -from ..exceptions import MissingDependencyError, SubprocessOutputError - -try: - from PIL import Image -except ImportError: - print("Could not find Python3 imaging library", file=sys.stderr) - raise - - -@lru_cache(maxsize=1) -def version(): - return get_version('unpaper') - - -def run(input_file, output_file, dpi, log, mode_args): - args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args - - SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'} - - im = Image.open(input_file) - if im.mode not in SUFFIXES.keys(): - log.info("Converting image to other colorspace") - try: - if im.mode == 'P' and len(im.getcolors()) == 2: - im = im.convert(mode='1') - else: - im = im.convert(mode='RGB') - except IOError as e: - log.error("Could not convert image with type " + im.mode) - im.close() - raise MissingDependencyError() from e - - try: - suffix = SUFFIXES[im.mode] - except KeyError: - log.error("Failed to convert image to a supported format.") - im.close() - raise MissingDependencyError() from e - - with TemporaryDirectory() as tmpdir: - input_pnm = os.path.join(tmpdir, f'input{suffix}') - output_pnm = os.path.join(tmpdir, f'output{suffix}') - im.save(input_pnm, format='PPM') - im.close() - - # To prevent any shenanigans from accepting arbitrary parameters in - # --unpaper-args, we: - # 1) run with cwd set to a tmpdir with only unpaper's files - # 2) forbid the use of '/' in arguments, to prevent changing paths - # 3) append absolute paths for the input and output file - # This should ensure that a user cannot clobber some other file with - # their unpaper arguments (whether intentionally or otherwise) - args_unpaper.extend([input_pnm, output_pnm]) - try: - proc = subprocess.run( - args_unpaper, - check=True, - close_fds=True, - universal_newlines=True, - stderr=STDOUT, - cwd=tmpdir, - stdout=PIPE, - ) - except CalledProcessError as e: - log.debug(e.output) - raise e from e - else: - log.debug(proc.stdout) - # unpaper sets dpi to 72; fix this - try: - Image.open(output_pnm).save(output_file, dpi=(dpi, dpi)) - except (FileNotFoundError, OSError): - raise SubprocessOutputError( - "unpaper: failed to produce the expected output file. Called with: " - + str(args_unpaper) - ) from None - - -def validate_custom_args(args: str): - unpaper_args = shlex.split(args) - if any('/' in arg for arg in unpaper_args): - raise ValueError('No filenames allowed in --unpaper-args') - return unpaper_args - - -def clean(input_file, output_file, dpi, log, unpaper_args=None): - default_args = [ - '--layout', - 'none', - '--mask-scan-size', - '100', # don't blank out narrow columns - '--no-border-align', # don't align visible content to borders - '--no-mask-center', # don't center visible content within page - '--no-grayfilter', # don't remove light gray areas - '--no-blackfilter', # don't remove solid black areas - '--no-deskew', # don't deskew - ] - if not unpaper_args: - unpaper_args = default_args - run(input_file, output_file, dpi, log, unpaper_args) diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/src/ocrmypdf/extra_plugins/__init__.py similarity index 100% rename from tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to src/ocrmypdf/extra_plugins/__init__.py diff --git a/src/ocrmypdf/extra_plugins/semfree.py b/src/ocrmypdf/extra_plugins/semfree.py new file mode 100644 index 00000000..c84b1b2d --- /dev/null +++ b/src/ocrmypdf/extra_plugins/semfree.py @@ -0,0 +1,191 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Semaphore-free alternate executor. + +There are two popular environments that do not fully support the standard Python +multiprocessing module: AWS Lambda, and Termux (a terminal emulator for Android). + +This alternate executor divvies up work among worker processes before processing, +rather than having each worker consume work from a shared queue when they finish +their task. This means workers have no need to coordinate with each other. Each +worker communicates only with the main process. + +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. +It is less efficient than the standard implementation, so not th edefault. +""" + +import logging +import logging.handlers +import signal +from contextlib import suppress +from enum import Enum, auto +from itertools import islice, repeat, takewhile, zip_longest +from multiprocessing import Pipe, Process +from multiprocessing.connection import Connection, wait +from typing import Callable, Iterable, Iterator + +from ocrmypdf import Executor, hookimpl +from ocrmypdf._concurrent import NullProgressBar +from ocrmypdf.exceptions import InputFileError +from ocrmypdf.helpers import remove_all_log_handlers + + +class MessageType(Enum): + exception = auto() + result = auto() + complete = auto() + + +def split_every(n: int, iterable: Iterable) -> Iterator: + """Split iterable into groups of n. + + >>> list(split_every(4, range(10))) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + https://stackoverflow.com/a/22919323 + """ + iterator = iter(iterable) + return takewhile(bool, (list(islice(iterator, n)) for _ in repeat(None))) + + +def process_sigbus(*args): + raise InputFileError("A worker process lost access to an input file") + + +class ConnectionLogHandler(logging.handlers.QueueHandler): + def __init__(self, conn: Connection) -> None: + super().__init__(None) + self.conn = conn + + def enqueue(self, record): + self.conn.send(('log', record)) + + +def process_loop( + conn: Connection, user_init: Callable[[], None], loglevel, task, task_args +): + """Initialize a process pool worker""" + + # Install SIGBUS handler (so our parent process can abort somewhat gracefully) + with suppress(AttributeError): # Windows and Cygwin do not have SIGBUS + # Windows and Cygwin do not have pthread_sigmask or SIGBUS + signal.signal(signal.SIGBUS, process_sigbus) + + # Reconfigure the root logger for this process to send all messages to a queue + h = ConnectionLogHandler(conn) + root = logging.getLogger() + remove_all_log_handlers(root) + root.setLevel(loglevel) + root.addHandler(h) + + user_init() + + for args in task_args: + try: + result = task(args) + except Exception as e: + conn.send((MessageType.exception, e)) + break + else: + conn.send((MessageType.result, result)) + + conn.send((MessageType.complete, None)) + conn.close() + return + + +class LambdaExecutor(Executor): + def _execute( + self, + *, + use_threads: bool, + max_workers: int, + tqdm_kwargs: dict, + worker_initializer: Callable, + task: Callable, + task_arguments: Iterable, + task_finished: Callable, + ): + if use_threads and max_workers == 1: + with self.pbar_class(**tqdm_kwargs) as pbar: + for args in task_arguments: + result = task(args) + task_finished(result, pbar) + return + + task_arguments = list(task_arguments) + grouped_args = list( + zip_longest(*list(split_every(max_workers, task_arguments))) + ) + if not grouped_args: + return + + processes = [] + connections = [] + for chunk in grouped_args: + parent_conn, child_conn = Pipe() + + worker_args = [args for args in chunk if args is not None] + process = Process( + target=process_loop, + args=( + child_conn, + worker_initializer, + logging.getLogger("").level, + task, + worker_args, + ), + ) + process.daemon = True + processes.append(process) + connections.append(parent_conn) + + for process in processes: + process.start() + + with self.pbar_class(**tqdm_kwargs) as pbar: + while connections: + for r in wait(connections): + try: + msg_type, msg = r.recv() + except EOFError: + connections.remove(r) + continue + + if msg_type == MessageType.result: + if task_finished: + task_finished(msg, pbar) + elif msg_type == 'log': + record = msg + logger = logging.getLogger(record.name) + logger.handle(record) + elif msg_type == MessageType.complete: + connections.remove(r) + elif msg_type == MessageType.exception: + for process in processes: + process.terminate() + raise msg + + for process in processes: + process.join() + + +@hookimpl +def get_executor(progressbar_class): + return LambdaExecutor(pbar_class=progressbar_class) + + +@hookimpl +def get_logging_console(): + return logging.StreamHandler() + + +@hookimpl +def get_progressbar_class(): + return NullProgressBar diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index 3d56a1f5..d7080f34 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -1,46 +1,88 @@ # © 2016 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging import multiprocessing import os -import sys +import shutil import warnings +from collections import namedtuple from collections.abc import Iterable from contextlib import suppress -from functools import partial, wraps +from functools import wraps +from io import StringIO +from math import isclose, isfinite from pathlib import Path +from typing import Any, Sequence + +import pikepdf + +log = logging.getLogger(__name__) -def re_symlink(input_file, soft_link_name, log=None): - """ - Helper function: relinks soft symbolic link if necessary +class Resolution(namedtuple('Resolution', ('x', 'y'))): + """The number of pixels per inch in each 2D direction.""" + + __slots__ = () + + def round(self, ndigits: int): + return Resolution(round(self.x, ndigits), round(self.y, ndigits)) + + def to_int(self): + return Resolution(int(round(self.x)), int(round(self.y))) + + @property + def is_square(self) -> bool: + return isclose(self.x, self.y, rel_tol=1e-3) + + @property + def is_finite(self) -> bool: + return isfinite(self.x) and isfinite(self.y) + + def take_max(self, vals, yvals=None): + if yvals is not None: + return Resolution(max(self.x, *vals), max(self.y, *yvals)) + max_x, max_y = self.x, self.y + for x, y in vals: + max_x = max(x, max_x) + max_y = max(y, max_y) + return Resolution(max_x, max_y) + + def flip_axis(self): + return Resolution(self.y, self.x) + + def __str__(self): + return f"{self.x:f}x{self.y:f}" + + def __repr__(self): # pragma: no cover + return f"Resolution({self.x}x{self.y} dpi)" + + +class NeverRaise(Exception): + """An exception that is never raised""" + + +def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike): + """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. + + Use symlinks safely. Self-linking loops are prevented. On Windows, file copy is + used since symlinks may require administrator privileges. An existing link at the + destination is removed. """ input_file = os.fspath(input_file) soft_link_name = os.fspath(soft_link_name) - if log is None: - prdebug = partial(print, file=sys.stderr) - else: - prdebug = log.debug # Guard against soft linking to oneself if input_file == soft_link_name: - prdebug( - "Warning: No symbolic link made. You are using " - + "the original data directory as the working directory." + log.warning( + "No symbolic link created. You are using the original data directory " + "as the working directory." ) return @@ -48,90 +90,165 @@ def re_symlink(input_file, soft_link_name, log=None): if os.path.lexists(soft_link_name): # do not delete or overwrite real (non-soft link) file if not os.path.islink(soft_link_name): - raise FileExistsError("%s exists and is not a link" % soft_link_name) - try: - os.unlink(soft_link_name) - except OSError: - prdebug("Can't unlink %s" % (soft_link_name)) + raise FileExistsError(f"{soft_link_name} exists and is not a link") + os.unlink(soft_link_name) if not os.path.exists(input_file): - raise FileNotFoundError("trying to create a broken symlink to %s" % input_file) + raise FileNotFoundError(f"trying to create a broken symlink to {input_file}") - prdebug("os.symlink(%s, %s)" % (input_file, soft_link_name)) + if os.name == 'nt': + # Don't actually use symlinks on Windows due to permission issues + shutil.copyfile(input_file, soft_link_name) + return + + log.debug("os.symlink(%s, %s)", input_file, soft_link_name) # Create symbolic link using absolute path os.symlink(os.path.abspath(input_file), soft_link_name) -def is_iterable_notstr(thing): +def samefile(f1: os.PathLike, f2: os.PathLike): + if os.name == 'nt': + return f1 == f2 + else: + return os.path.samefile(f1, f2) + + +def is_iterable_notstr(thing: Any) -> bool: + """Is this is an iterable type, other than a string?""" return isinstance(thing, Iterable) and not isinstance(thing, str) -def page_number(input_file): +def monotonic(L: Sequence) -> bool: + """Does this sequence increase monotonically?""" + return all(b > a for a, b in zip(L, L[1:])) + + +def page_number(input_file: os.PathLike) -> int: """Get one-based page number implied by filename (000002.pdf -> 2)""" return int(os.path.basename(os.fspath(input_file))[0:6]) -def available_cpu_count(): +def available_cpu_count() -> int: + """Returns number of CPUs in the system.""" try: return multiprocessing.cpu_count() except NotImplementedError: pass - - try: - import psutil - - return psutil.cpu_count() - except (ImportError, AttributeError): - pass - warnings.warn( "Could not get CPU count. Assuming one (1) CPU." "Use -j N to set manually." ) return 1 -def is_file_writable(test_file): +def is_file_writable(test_file: os.PathLike) -> bool: """Intentionally racy test if target is writable. We intend to write to the output file if and only if we succeed and can replace it atomically. Before doing the OCR work, make sure the location is writable. """ - p = Path(test_file) + try: + p = Path(test_file) + if p.is_symlink(): + p = p.resolve(strict=False) - if p.is_symlink(): - p = p.resolve(strict=False) + # p.is_file() throws an exception in some cases + if p.exists() and p.is_file(): + return os.access( + os.fspath(p), + os.W_OK, + effective_ids=(os.access in os.supports_effective_ids), + ) + else: + try: + fp = p.open('wb') + except OSError: + return False + else: + fp.close() + with suppress(OSError): + p.unlink() + return True + except (EnvironmentError, RuntimeError) as e: + log.debug(e) + log.error(str(e)) + return False - # p.is_file() throws an exception in some cases - if p.exists() and p.is_file(): - return os.access( - os.fspath(p), - os.W_OK, - effective_ids=(os.access in os.supports_effective_ids), - ) + +def check_pdf(input_file: Path) -> bool: + """Check if a PDF complies with the PDF specification. + + Checks for proper formatting and proper linearization. Uses pikepdf (which in + turn, uses QPDF) to perform the checks. + """ + try: + pdf = pikepdf.open(input_file) + except pikepdf.PdfError as e: + log.error(e) + return False else: - try: - fp = p.open('wb') - except OSError: + with pdf: + messages = pdf.check() + for msg in messages: + if 'error' in msg.lower(): + log.error(msg) + else: + log.warning(msg) + + sio = StringIO() + linearize_msgs = '' + try: + # If linearization is missing entirely, we do not complain. We do + # complain if linearization is present but incorrect. + pdf.check_linearization(sio) + except RuntimeError: + pass + except ( + # Workaround for a problematic pikepdf version + # pragma: no cover + getattr(pikepdf, 'ForeignObjectError') + if pikepdf.__version__ == '2.1.0' + else NeverRaise + ): + pass + else: + linearize_msgs = sio.getvalue() + if linearize_msgs: + log.warning(linearize_msgs) + + if not messages and not linearize_msgs: + return True return False - else: - fp.close() - with suppress(OSError): - p.unlink() - return True -def flatten_groups(groups): - for obj in groups: - if is_iterable_notstr(obj): - yield from obj - else: - yield obj +def clamp(n, smallest, largest): # mypy doesn't understand types for this + """Clamps the value of ``n`` to between ``smallest`` and ``largest``.""" + return max(smallest, min(n, largest)) + + +def remove_all_log_handlers(logger): + "Remove all log handlers, usually used in a child process." + for handler in logger.handlers[:]: + logger.removeHandler(handler) + handler.close() # To ensure handlers with opened resources are released + + +def pikepdf_enable_mmap(): + # try: + # if pikepdf._qpdf.set_access_default_mmap(True): + # log.debug("pikepdf mmap enabled") + # except AttributeError: + # log.debug("pikepdf mmap not available") + # 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. + # Fix is not in pybind11 2.6.0 + # log.debug("pikepdf mmap disabled") + return def deprecated(func): - """Warn that function is deprecated""" + """Warn that function is deprecated.""" @wraps(func) def new_func(*args, **kwargs): diff --git a/src/ocrmypdf/hocrtransform.py b/src/ocrmypdf/hocrtransform.py index e7c28e54..6c64bcff 100755 --- a/src/ocrmypdf/hocrtransform.py +++ b/src/ocrmypdf/hocrtransform.py @@ -29,15 +29,28 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import argparse +import os import re -from collections import namedtuple +from itertools import chain from math import atan, cos, sin +from pathlib import Path +from typing import Any, NamedTuple, Optional, Tuple, Union from xml.etree import ElementTree +from reportlab.lib.colors import black, cyan, magenta, red from reportlab.lib.units import inch from reportlab.pdfgen.canvas import Canvas -Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2']) +Element = ElementTree.Element + + +class Rect(NamedTuple): # pylint: disable=inherit-non-class + """A rectangle for managing PDF coordinates.""" + + x1: Any + y1: Any + x2: Any + y2: Any class HocrTransformError(Exception): @@ -64,9 +77,9 @@ class HocrTransform: {'ff': 'ff', 'ffi': 'f‌f‌i', 'ffl': 'f‌f‌l', 'fi': 'fi', 'fl': 'fl'} ) - def __init__(self, hocrFileName, dpi): + def __init__(self, *, hocr_filename: Union[str, Path], dpi: float): self.dpi = dpi - self.hocr = ElementTree.parse(hocrFileName) + self.hocr = ElementTree.parse(os.fspath(hocr_filename)) # if the hOCR file has a namespace, ElementTree requires its use to # find elements @@ -77,7 +90,7 @@ class HocrTransform: # get dimension in pt (not pixel!!!!) of the OCRed image self.width, self.height = None, None - for div in self.hocr.findall(".//%sdiv[@class='ocr_page']" % (self.xmlns)): + for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')): coords = self.element_coordinates(div) pt_coords = self.pt_from_pixel(coords) self.width = pt_coords.x2 - pt_coords.x1 @@ -88,38 +101,38 @@ class HocrTransform: if self.width is None or self.height is None: raise HocrTransformError("hocr file is missing page dimensions") - def __str__(self): + def __str__(self): # pragma: no cover """ Return the textual content of the HTML body """ if self.hocr is None: return '' - body = self.hocr.find(".//%sbody" % (self.xmlns)) + body = self.hocr.find(self._child_xpath('body')) if body: return self._get_element_text(body) else: return '' - def _get_element_text(self, element): + def _get_element_text(self, element: Element): """ Return the textual content of the element and its children """ text = '' if element.text is not None: text += element.text - for child in element.getchildren(): + for child in element: text += self._get_element_text(child) if element.tail is not None: text += element.tail return text @classmethod - def element_coordinates(cls, element): + def element_coordinates(cls, element: Element) -> Rect: """ Returns a tuple containing the coordinates of the bounding box around an element """ - out = (0, 0, 0, 0) + out = Rect._make(0 for _ in range(4)) if 'title' in element.attrib: matches = cls.box_pattern.search(element.attrib['title']) if matches: @@ -128,7 +141,7 @@ class HocrTransform: return out @classmethod - def baseline(cls, element): + def baseline(cls, element: Element) -> Tuple[float, float]: """ Returns a tuple containing the baseline slope and intercept. """ @@ -136,32 +149,47 @@ class HocrTransform: matches = cls.baseline_pattern.search(element.attrib['title']) if matches: return float(matches.group(1)), int(matches.group(2)) - return (0, 0) + return (0.0, 0.0) - def pt_from_pixel(self, pxl): + def pt_from_pixel(self, pxl) -> Rect: """ Returns the quantity in PDF units (pt) given quantity in pixels """ return Rect._make((c / self.dpi * inch) for c in pxl) + def _child_xpath(self, html_tag: str, html_class: Optional[str] = None) -> str: + xpath = f".//{self.xmlns}{html_tag}" + if html_class: + xpath += f"[@class='{html_class}']" + return xpath + @classmethod - def replace_unsupported_chars(cls, s): + def replace_unsupported_chars(cls, s: str) -> str: """ Given an input string, returns the corresponding string that: - - is available in the helvetica facetype - - does not contain any ligature (to allow easy search in the PDF file) + * is available in the Helvetica facetype + * does not contain any ligature (to allow easy search in the PDF file) """ return s.translate(cls.ligatures) + def topdown_position(self, element): + pxl_line_coords = self.element_coordinates(element) + line_box = self.pt_from_pixel(pxl_line_coords) + # Coordinates here are still in the hocr coordinate system, so 0 on the y axis + # is the top of the page and increasing values of y will move towards the + # bottom of the page. + return line_box.y2 + def to_pdf( self, - outFileName, - imageFileName=None, - showBoundingboxes=False, - fontname="Helvetica", - invisibleText=False, - interwordSpaces=False, - ): + *, + out_filename: Path, + image_filename: Optional[Path] = None, + show_bounding_boxes: bool = False, + fontname: str = "Helvetica", + invisible_text: bool = False, + interword_spaces: bool = False, + ) -> None: """ Creates a PDF file with an image superimposed on top of the text. Text is positioned according to the bounding box of the lines in @@ -169,19 +197,36 @@ class HocrTransform: The image need not be identical to the image used to create the hOCR file. It can have a lower resolution, different color mode, etc. + + Arguments: + out_filename: Path of PDF to write. + image_filename: Image to use for this file. If omitted, the OCR text + is shown. + show_bounding_boxes: Show bounding boxes around various text regions, + for debugging. + fontname: Name of font to use. + invisible_text: If True, text is rendered invisible so that is + selectable but never drawn. If False, text is visible and may + be seen if the image is skipped or deleted in Acrobat. + interword_spaces: If True, insert spaces between words rather than + drawing each word without spaces. Generally this improves text + extraction. """ # create the PDF file # page size in points (1/72 in.) - pdf = Canvas(outFileName, pagesize=(self.width, self.height), pageCompression=1) + pdf = Canvas( + os.fspath(out_filename), + pagesize=(self.width, self.height), + pageCompression=1, + ) # draw bounding box for each paragraph # light blue for bounding box of paragraph - pdf.setStrokeColorRGB(0, 1, 1) + pdf.setStrokeColor(cyan) # light blue for bounding box of paragraph - pdf.setFillColorRGB(0, 1, 1) + pdf.setFillColor(cyan) pdf.setLineWidth(0) # no line for bounding box - for elem in self.hocr.findall(".//%sp[@class='%s']" % (self.xmlns, "ocr_par")): - + for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')): elemtxt = self._get_element_text(elem).rstrip() if len(elemtxt) == 0: continue @@ -190,14 +235,19 @@ class HocrTransform: pt = self.pt_from_pixel(pxl_coords) # draw the bbox border - if showBoundingboxes: + if show_bounding_boxes: # pragma: no cover pdf.rect( pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1 ) found_lines = False - for line in self.hocr.findall( - ".//%sspan[@class='%s']" % (self.xmlns, "ocr_line") + for line in sorted( + chain( + self.hocr.iterfind(self._child_xpath('span', 'ocr_header')), + self.hocr.iterfind(self._child_xpath('span', 'ocr_line')), + self.hocr.iterfind(self._child_xpath('span', 'ocr_textfloat')), + ), + key=self.topdown_position, ): found_lines = True self._do_line( @@ -205,45 +255,49 @@ class HocrTransform: line, "ocrx_word", fontname, - invisibleText, - interwordSpaces, - showBoundingboxes, + invisible_text, + interword_spaces, + show_bounding_boxes, ) if not found_lines: # Tesseract did not report any lines (just words) - root = self.hocr.find(".//%sdiv[@class='%s']" % (self.xmlns, "ocr_page")) + root = self.hocr.find(self._child_xpath('div', 'ocr_page')) self._do_line( pdf, root, "ocrx_word", fontname, - invisibleText, - interwordSpaces, - showBoundingboxes, + invisible_text, + interword_spaces, + show_bounding_boxes, ) # put the image on the page, scaled to fill the page - if imageFileName is not None: - pdf.drawImage(imageFileName, 0, 0, width=self.width, height=self.height) + if image_filename is not None: + pdf.drawImage( + os.fspath(image_filename), 0, 0, width=self.width, height=self.height + ) # finish up the page and save it pdf.showPage() pdf.save() @classmethod - def polyval(cls, poly, x): + def polyval(cls, poly, x): # pragma: no cover return x * poly[0] + poly[1] def _do_line( self, - pdf, - line, - elemclass, - fontname, - invisibleText, - interwordSpaces, - showBoundingboxes, + pdf: Canvas, + line: Optional[Element], + elemclass: str, + fontname: str, + invisible_text: bool, + interword_spaces: bool, + show_bounding_boxes: bool, ): + if not line: + return pxl_line_coords = self.element_coordinates(line) line_box = self.pt_from_pixel(pxl_line_coords) line_height = line_box.y2 - line_box.y1 @@ -262,17 +316,17 @@ class HocrTransform: # on a sloped baseline and the edge of the bounding box. fontsize = (line_height - abs(intercept)) / cos_a text.setFont(fontname, fontsize) - if invisibleText: + if invisible_text: text.setTextRenderMode(3) # Invisible (indicates OCR text) # Intercept is normally negative, so this places it above the bottom # of the line box baseline_y2 = self.height - (line_box.y2 + intercept) - if showBoundingboxes: + if show_bounding_boxes: # pragma: no cover # draw the baseline in magenta, dashed pdf.setDash() - pdf.setStrokeColorRGB(0.95, 0.65, 0.95) + pdf.setStrokeColor(magenta) pdf.setLineWidth(0.5) # negate slope because it is defined as a rise/run in pixel # coordinates and page coordinates have the y axis flipped @@ -284,12 +338,12 @@ class HocrTransform: ) # light green for bounding box of word/line pdf.setDash(6, 3) - pdf.setStrokeColorRGB(1, 0, 0) + pdf.setStrokeColor(red) text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2) - pdf.setFillColorRGB(0, 0, 0) # text in black + pdf.setFillColor(black) # text in black - elements = line.findall(".//%sspan[@class='%s']" % (self.xmlns, elemclass)) + elements = line.findall(self._child_xpath('span', elemclass)) for elem in elements: elemtxt = self._get_element_text(elem).strip() elemtxt = self.replace_unsupported_chars(elemtxt) @@ -298,7 +352,7 @@ class HocrTransform: pxl_coords = self.element_coordinates(elem) box = self.pt_from_pixel(pxl_coords) - if interwordSpaces: + if interword_spaces: # if `--interword-spaces` is true, append a space # to the end of each text element to allow simpler PDF viewers # such as PDF.js to better recognize words in search and copy @@ -318,7 +372,7 @@ class HocrTransform: font_width = pdf.stringWidth(elemtxt, fontname, fontsize) # draw the bbox border - if showBoundingboxes: + if show_bounding_boxes: # pragma: no cover pdf.rect( box.x1, self.height - line_box.y2, box_width, line_height, fill=0 ) @@ -380,10 +434,10 @@ if __name__ == "__main__": parser.add_argument('outputfile', help='Path to the PDF file to be generated') args = parser.parse_args() - hocr = HocrTransform(args.hocrfile, args.resolution) + hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution) hocr.to_pdf( - args.outputfile, - args.image, - args.boundingboxes, - interwordSpaces=args.interword_spaces, + out_filename=args.outputfile, + image_filename=args.image, + show_bounding_boxes=args.boundingboxes, + interword_spaces=args.interword_spaces, ) diff --git a/src/ocrmypdf/leptonica.py b/src/ocrmypdf/leptonica.py index 1cdec312..807560ff 100644 --- a/src/ocrmypdf/leptonica.py +++ b/src/ocrmypdf/leptonica.py @@ -3,20 +3,10 @@ # # © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83) # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + # # Python FFI wrapper for Leptonica library @@ -24,30 +14,83 @@ import argparse import logging import os import sys -import warnings +import threading +from collections import deque from collections.abc import Sequence from contextlib import suppress from ctypes.util import find_library from functools import lru_cache -from io import BytesIO +from io import BytesIO, UnsupportedOperation from os import fspath from tempfile import TemporaryFile +from warnings import warn -from .lib._leptonica import ffi +from ocrmypdf.exceptions import MissingDependencyError +from ocrmypdf.lib._leptonica import ffi # pylint: disable=protected-access logger = logging.getLogger(__name__) -lept = ffi.dlopen(find_library('lept')) -lept.setMsgSeverity(lept.L_SEVERITY_WARNING) +if os.name == 'nt': + from ocrmypdf.subprocess._windows import shim_env_path + + libname = 'liblept-5' + os.environ['PATH'] = shim_env_path() +else: + libname = 'lept' +_libpath = find_library(libname) +if not _libpath: + raise MissingDependencyError( + """ + --------------------------------------------------------------------- + This error normally occurs when ocrmypdf can't find the Leptonica + library, which is usually installed with Tesseract OCR. It could be that + Tesseract is not installed properly, we can't find the installation + on your system PATH environment variable. + + The library we are looking for is usually called: + liblept-5.dll (Windows) + liblept*.dylib (macOS) + liblept*.so (Linux/BSD) + + Please review our installation procedures to find a solution: + https://ocrmypdf.readthedocs.io/en/latest/installation.html + --------------------------------------------------------------------- + """ + ) +if os.name == 'nt': + # On Windows, recent versions of libpng require zlib. We have to make sure + # the zlib version being loaded is the same one that libpng was built with. + # This tries to import zlib from Tesseract's installation folder, falling back + # to find_library() if liblept is being loaded from somewhere else. + # Loading zlib from other places could cause a version mismatch + _zlib_path = os.path.join(os.path.dirname(_libpath), 'zlib1.dll') + if not os.path.exists(_zlib_path): + _zlib_path = find_library('zlib') + try: + zlib = ffi.dlopen(_zlib_path) + except ffi.error as e: + raise MissingDependencyError( + """ + Could not load the zlib library. It could be that Tesseract is not installed properly, + we can't find the installation on your system PATH environment variable. + """ + ) from e +try: + lept = ffi.dlopen(_libpath) + lept.setMsgSeverity(lept.L_SEVERITY_WARNING) +except ffi.error as e: + raise MissingDependencyError( + f"Leptonica library found at {_libpath}, but we could not access it" + ) from e -class _LeptonicaErrorTrap: +class _LeptonicaErrorTrap_Redirect: """ - Context manager to trap errors reported by Leptonica. + Context manager to trap errors reported by Leptonica < 1.79 or on Apple Silicon. - Leptonica's error return codes don't provide much informatino about what + Leptonica's error return codes don't provide much information about what went wrong. Leptonica does, however, write more detailed errors to stderr (provided this is not disabled at compile time). The Leptonica source code is very consistent in its use of macros to generate errors. @@ -58,20 +101,23 @@ class _LeptonicaErrorTrap: """ + leptonica_lock = threading.Lock() + def __init__(self): self.tmpfile = None self.copy_of_stderr = -1 self.no_stderr = False def __enter__(self): - from io import UnsupportedOperation - self.tmpfile = TemporaryFile() # Save the old stderr, and redirect stderr to temporary file - with suppress(AttributeError): - sys.stderr.flush() + self.leptonica_lock.acquire() try: + # It would make sense to do sys.stderr.flush() here, but that can deadlock + # due to https://bugs.python.org/issue6721. So don't flush. Pretend + # there's nothing important in sys.stderr. If the user cared they would + # be using Leptonica 1.79 or later anyway to avoid this mess. self.copy_of_stderr = os.dup(sys.stderr.fileno()) os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(), inheritable=False) except AttributeError: @@ -83,7 +129,10 @@ class _LeptonicaErrorTrap: os.dup2(self.tmpfile.fileno(), 2, inheritable=False) except UnsupportedOperation: self.copy_of_stderr = None - return + except Exception: + self.leptonica_lock.release() + raise + return self def __exit__(self, exc_type, exc_value, traceback): # Restore old stderr @@ -100,6 +149,8 @@ class _LeptonicaErrorTrap: self.tmpfile.seek(0) # Cursor will be at end, so move back to beginning leptonica_output = self.tmpfile.read().decode(errors='replace') self.tmpfile.close() + self.leptonica_lock.release() + # If there are Python errors, record them if exc_type: logger.warning(leptonica_output) @@ -117,6 +168,70 @@ class _LeptonicaErrorTrap: return False +tls = threading.local() +tls.trap = None + + +class _LeptonicaErrorTrap_Queue: + def __init__(self): + self.queue = deque() + + def __enter__(self): + self.queue.clear() + tls.trap = self.queue + + def __exit__(self, exc_type, exc_value, traceback): + tls.trap = None + output = ''.join(self.queue) + self.queue.clear() + + # If there are Python errors, record them + if exc_type: + logger.warning(output) + + if 'Error' in output: + if 'image file not found' in output: + raise FileNotFoundError() + elif 'pixWrite: stream not opened' in output: + raise LeptonicaIOError() + elif 'index not valid' in output: + raise IndexError() + elif 'pixGetInvBackgroundMap: w and h must be >= 5' in output: + logger.warning( + "Leptonica attempted to remove background from a low resolution - " + "you may want to review in a PDF viewer" + ) + else: + raise LeptonicaError(output) + return False + + +try: + + @ffi.callback("void(char *)") + def _stderr_handler(cstr): + msg = ffi.string(cstr).decode(errors='replace') + if msg.startswith("Error"): + logger.error(msg) + elif msg.startswith("Warning"): + logger.warning(msg) + else: + logger.debug(msg) + if tls.trap is not None: + tls.trap.append(msg) + return + + lept.leptSetStderrHandler(_stderr_handler) +except (ffi.error, MemoryError): + # Pre-1.79 Leptonica does not have leptSetStderrHandler + # And some platforms, notably Apple ARM 64, do not allow the write+execute + # memory needed to set up the callback function. + _LeptonicaErrorTrap = _LeptonicaErrorTrap_Redirect +else: + # 1.79 have this new symbol + _LeptonicaErrorTrap = _LeptonicaErrorTrap_Queue + + class LeptonicaError(Exception): pass @@ -282,7 +397,7 @@ class Pix(LeptonicaObject): @classmethod def read(cls, path): - warnings.warn('Use Pix.open() instead', DeprecationWarning) + warn('Use Pix.open() instead', DeprecationWarning) return cls.open(path) @classmethod @@ -292,9 +407,11 @@ class Pix(LeptonicaObject): Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If loading fails then the object will wrap a C null pointer. """ - filename = fspath(path) - with _LeptonicaErrorTrap(): - return cls(lept.pixRead(os.fsencode(filename))) + with open(path, 'rb') as py_file: + data = py_file.read() + buffer = ffi.from_buffer(data) + with _LeptonicaErrorTrap(): + return cls(lept.pixReadMem(buffer, len(buffer))) def write_implied_format(self, path, jpeg_quality=0, jpeg_progressive=0): """Write pix to the filename, with the extension indicating format. @@ -302,14 +419,22 @@ class Pix(LeptonicaObject): jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default) jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive) """ - filename = fspath(path) - with _LeptonicaErrorTrap(): - lept.pixWriteImpliedFormat( - os.fsencode(filename), self._cdata, jpeg_quality, jpeg_progressive - ) + lept_format = lept.getImpliedFileFormat(os.fsencode(path)) + with open(path, 'wb') as py_file: + data = ffi.new('l_uint8 **pdata') + size = ffi.new('size_t *psize') + with _LeptonicaErrorTrap(): + if lept_format == lept.L_JPEG_ENCODE: + lept.pixWriteMemJpeg( + data, size, self._cdata, jpeg_quality, jpeg_progressive + ) + else: + lept.pixWriteMem(data, size, self._cdata, lept_format) + buffer = ffi.buffer(data[0], size[0]) + py_file.write(buffer) @classmethod - def frompil(self, pillow_image): + def frompil(cls, pillow_image): """Create a copy of a PIL.Image from this Pix""" bio = BytesIO() pillow_image.save(bio, format='png', compress_level=1) @@ -321,7 +446,7 @@ class Pix(LeptonicaObject): def topil(self): """Returns a PIL.Image version of this Pix""" - from PIL import Image + from PIL import Image # pylint: disable=import-outside-toplevel # Leptonica manages data in words, so it implicitly does an endian # swap. Tell Pillow about this when it reads the data. @@ -492,27 +617,15 @@ class Pix(LeptonicaObject): ) return Pix(thresh_pix) - def crop_to_foreground( - self, - threshold=128, - mindist=70, - erasedist=30, - pagenum=0, - showmorph=0, - display=0, - pdfdir=ffi.NULL, - ): + def crop_to_foreground(self, threshold=128, mindist=70, erasedist=30, showmorph=0): + if get_leptonica_version() < 'leptonica-1.76': + # Leptonica 1.76 changed the API for pixFindPageForeground; we don't + # support the old version + raise LeptonicaError("Not available in this version of Leptonica") with _LeptonicaErrorTrap(): cropbox = Box( lept.pixFindPageForeground( - self._cdata, - threshold, - mindist, - erasedist, - pagenum, - showmorph, - display, - pdfdir, + self._cdata, threshold, mindist, erasedist, showmorph, ffi.NULL ) ) @@ -549,6 +662,9 @@ class Pix(LeptonicaObject): bg_val=200, smooth_kernel=(2, 1), ): + if self.width < tile_size[0] or self.height < tile_size[1]: + logger.info("Skipped pixMaskedThreshOnBackgroundNorm on small image") + return self # Background norm doesn't work on color mapped Pix, so remove colormap target_pix = self.remove_colormap(lept.REMOVE_CMAP_BASED_ON_SRC) with _LeptonicaErrorTrap(): @@ -827,6 +943,8 @@ def get_leptonica_version(): Caveat: Leptonica expects the caller to free this memory. We don't, since that would involve binding to libc to access libc.free(), a pointless effort to reclaim 100 bytes of memory. + + Reminder that this returns "leptonica-1.xx" or "leptonica-1.yy.0". """ return ffi.string(lept.getLeptonicaVersion()).decode() diff --git a/src/ocrmypdf/lib/__init__.py b/src/ocrmypdf/lib/__init__.py index 06ca8523..45460814 100644 --- a/src/ocrmypdf/lib/__init__.py +++ b/src/ocrmypdf/lib/__init__.py @@ -1,18 +1,8 @@ # © 2017 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + """Bindings to external libraries""" diff --git a/src/ocrmypdf/lib/_leptonica.py b/src/ocrmypdf/lib/_leptonica.py index 17a2f757..996d5f8a 100644 --- a/src/ocrmypdf/lib/_leptonica.py +++ b/src/ocrmypdf/lib/_leptonica.py @@ -3,9 +3,9 @@ import _cffi_backend ffi = _cffi_backend.FFI('ocrmypdf.lib._leptonica', _version = 0x2601, - _types = b'\x00\x00\x01\x0D\x00\x01\x33\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x34\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x37\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x3F\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x38\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x09\x03\x00\x00\x1A\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x01\x11\x00\x00\x01\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x01\x3C\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x05\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x60\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x13\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x10\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x4E\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x50\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x13\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x3A\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x3A\x03\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x3A\x0D\x00\x00\x13\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x9C\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x45\x0D\x00\x00\x10\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x10\x11\x00\x00\x00\x0F\x00\x00\x45\x0D\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x3E\x0D\x00\x00\x45\x11\x00\x00\x00\x0F\x00\x01\x3E\x0D\x00\x00\x00\x0F\x00\x00\x60\x0D\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x04\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x32\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x60\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xC3\x11\x00\x00\xC3\x11\x00\x00\xC3\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\xC3\x11\x00\x00\xC3\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x60\x11\x00\x00\x60\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x60\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x35\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xC3\x11\x00\x00\xC3\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x1A\x11\x00\x00\x1A\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x01\x4F\x03\x00\x00\x8E\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x10\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\xEC\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x10\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x01\x4D\x03\x00\x01\x04\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x23\x11\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x04\x03\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\xEC\x11\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x1A\x11\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x13\x03\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x9C\x11\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x00\x45\x03\x00\x00\x00\x0F\x00\x01\x53\x0D\x00\x01\x53\x03\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x01\x36\x03\x00\x00\x02\x09\x00\x00\x03\x09\x00\x00\x05\x09\x00\x00\x04\x09\x00\x01\x3B\x03\x00\x00\x06\x09\x00\x00\x07\x09\x00\x01\x3E\x03\x00\x01\x3F\x03\x00\x00\x02\x01\x00\x00\x0E\x01\x00\x00\x00\x0B\x00\x00\x01\x0B\x00\x00\x02\x0B\x00\x00\x03\x0B\x00\x00\x04\x0B\x00\x00\x05\x0B\x00\x00\x06\x0B\x00\x00\x60\x03\x00\x00\x0B\x01\x00\x00\x05\x01\x00\x00\x03\x01\x00\x01\x39\x03\x00\x01\x4E\x03\x00\x00\x04\x01\x00\x01\x50\x03\x00\x00\x08\x01\x00\x00\x0C\x01\x00\x00\x06\x01\x00\x00\x00\x01', - _globals = (b'\xFF\xFF\xFF\x0BL_BF_ANY',1,b'\xFF\xFF\xFF\x0BL_BF_CODABAR',9,b'\xFF\xFF\xFF\x0BL_BF_CODE128',2,b'\xFF\xFF\xFF\x0BL_BF_CODE2OF5',5,b'\xFF\xFF\xFF\x0BL_BF_CODE39',7,b'\xFF\xFF\xFF\x0BL_BF_CODE93',8,b'\xFF\xFF\xFF\x0BL_BF_CODEI2OF5',6,b'\xFF\xFF\xFF\x0BL_BF_EAN13',4,b'\xFF\xFF\xFF\x0BL_BF_EAN8',3,b'\xFF\xFF\xFF\x0BL_BF_UNKNOWN',0,b'\xFF\xFF\xFF\x0BL_BF_UPCA',10,b'\xFF\xFF\xFF\x0BL_CLONE',2,b'\xFF\xFF\xFF\x0BL_COPY',1,b'\xFF\xFF\xFF\x0BL_COPY_CLONE',3,b'\xFF\xFF\xFF\x0BL_DEFAULT_ENCODE',0,b'\xFF\xFF\xFF\x0BL_FLATE_ENCODE',3,b'\xFF\xFF\xFF\x0BL_G4_ENCODE',2,b'\xFF\xFF\xFF\x0BL_INSERT',0,b'\xFF\xFF\xFF\x0BL_JP2K_ENCODE',4,b'\xFF\xFF\xFF\x0BL_JPEG_ENCODE',1,b'\xFF\xFF\xFF\x0BL_NOCOPY',0,b'\xFF\xFF\xFF\x0BL_SEVERITY_ALL',1,b'\xFF\xFF\xFF\x0BL_SEVERITY_DEBUG',2,b'\xFF\xFF\xFF\x0BL_SEVERITY_ERROR',5,b'\xFF\xFF\xFF\x0BL_SEVERITY_EXTERNAL',0,b'\xFF\xFF\xFF\x0BL_SEVERITY_INFO',3,b'\xFF\xFF\xFF\x0BL_SEVERITY_NONE',6,b'\xFF\xFF\xFF\x0BL_SEVERITY_WARNING',4,b'\xFF\xFF\xFF\x0BL_USE_WIDTHS',1,b'\xFF\xFF\xFF\x0BL_USE_WINDOWS',2,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_BASED_ON_SRC',4,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_BINARY',0,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_FULL_COLOR',2,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_GRAYSCALE',1,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_WITH_ALPHA',3,b'\xFF\xFF\xFF\x0BSEL_DONT_CARE',0,b'\xFF\xFF\xFF\x0BSEL_HIT',1,b'\xFF\xFF\xFF\x0BSEL_MISS',2,b'\x00\x00\x00\x23boxClone',0,b'\x00\x01\x1B\x23boxDestroy',0,b'\x00\x01\x1E\x23boxaDestroy',0,b'\x00\x00\x03\x23boxaGetBox',0,b'\x00\x00\xB6\x23getLeptonicaVersion',0,b'\x00\x01\x21\x23l_CIDataDestroy',0,b'\x00\x01\x06\x23l_generateCIDataForPdf',0,b'\x00\x01\x30\x23lept_free',0,b'\x00\x00\xB8\x23makePixelSumTab8',0,b'\x00\x00\x29\x23pixAnd',0,b'\x00\x00\x36\x23pixBackgroundNorm',0,b'\x00\x00\x2E\x23pixCleanBackgroundToWhite',0,b'\x00\x00\x20\x23pixClipRectangle',0,b'\x00\x00\xEE\x23pixColorFraction',0,b'\x00\x00\x7D\x23pixColorMagnitude',0,b'\x00\x00\x1D\x23pixConvertRGBToLuminance',0,b'\x00\x00\x74\x23pixConvertTo8',0,b'\x00\x00\xC0\x23pixCorrelationBinary',0,b'\x00\x00\xDA\x23pixCountPixels',0,b'\x00\x00\x90\x23pixDeserializeFromMemory',0,b'\x00\x00\x74\x23pixDeskew',0,b'\x00\x01\x24\x23pixDestroy',0,b'\x00\x00\x42\x23pixDilate',0,b'\x00\x00\x1D\x23pixEndianByteSwapNew',0,b'\x00\x00\xC5\x23pixEqual',0,b'\x00\x00\x42\x23pixErode',0,b'\x00\x00\x94\x23pixExtractBarcodes',0,b'\x00\x00\x08\x23pixFindPageForeground',0,b'\x00\x00\xD5\x23pixFindSkew',0,b'\x00\x00\x47\x23pixGammaTRC',0,b'\x00\x00\xE7\x23pixGenerateCIData',0,b'\x00\x00\xCA\x23pixGetAverageMaskedRGB',0,b'\x00\x00\x4E\x23pixGlobalNormRGB',0,b'\x00\x00\x42\x23pixHMT',0,b'\x00\x00\x25\x23pixInvert',0,b'\x00\x00\x17\x23pixLocateBarcodes',0,b'\x00\x00\x78\x23pixMaskOverColorPixels',0,b'\x00\x00\x56\x23pixMaskedThreshOnBackgroundNorm',0,b'\x00\x00\xDF\x23pixNumSignificantGrayColors',0,b'\x00\x00\xF7\x23pixOtsuAdaptiveThreshold',0,b'\x00\x00\x62\x23pixOtsuThreshOnBackgroundNorm',0,b'\x00\x00\x98\x23pixProcessBarcodes',0,b'\x00\x00\x89\x23pixRead',0,b'\x00\x00\x9F\x23pixReadBarcodes',0,b'\x00\x00\x8C\x23pixReadMem',0,b'\x00\x00\x74\x23pixRemoveColormap',0,b'\x00\x00\x78\x23pixRemoveColormapGeneral',0,b'\x00\x00\xBA\x23pixRenderBoxa',0,b'\x00\x00\x25\x23pixRotate180',0,b'\x00\x00\x74\x23pixRotateOrth',0,b'\x00\x00\x6F\x23pixScale',0,b'\x00\x01\x01\x23pixSerializeToMemory',0,b'\x00\x00\x29\x23pixSubtract',0,b'\x00\x01\x0C\x23pixWriteImpliedFormat',0,b'\x00\x01\x15\x23pixWriteMemPng',0,b'\x00\x01\x27\x23pixaDestroy',0,b'\x00\x00\x12\x23pixaGetBox',0,b'\x00\x00\x84\x23pixaGetPix',0,b'\x00\x01\x2A\x23sarrayDestroy',0,b'\x00\x00\xAC\x23selCreateBrick',0,b'\x00\x00\xA6\x23selCreateFromString',0,b'\x00\x01\x2D\x23selDestroy',0,b'\x00\x00\xB3\x23selPrintToString',0,b'\x00\x01\x12\x23setMsgSeverity',0), - _struct_unions = ((b'\x00\x00\x01\x33\x00\x00\x00\x02Box',b'\x00\x00\x05\x11x',b'\x00\x00\x05\x11y',b'\x00\x00\x05\x11w',b'\x00\x00\x05\x11h',b'\x00\x01\x50\x11refcount'),(b'\x00\x00\x01\x34\x00\x00\x00\x02Boxa',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11nalloc',b'\x00\x01\x50\x11refcount',b'\x00\x00\x23\x11box'),(b'\x00\x00\x01\x36\x00\x00\x00\x02L_Compressed_Data',b'\x00\x00\x05\x11type',b'\x00\x01\x4D\x11datacomp',b'\x00\x00\x8E\x11nbytescomp',b'\x00\x01\x3E\x11data85',b'\x00\x00\x8E\x11nbytes85',b'\x00\x01\x3E\x11cmapdata85',b'\x00\x01\x3E\x11cmapdatahex',b'\x00\x00\x05\x11ncolors',b'\x00\x00\x05\x11w',b'\x00\x00\x05\x11h',b'\x00\x00\x05\x11bps',b'\x00\x00\x05\x11spp',b'\x00\x00\x05\x11minisblack',b'\x00\x00\x05\x11predictor',b'\x00\x00\x8E\x11nbytes',b'\x00\x00\x05\x11res'),(b'\x00\x00\x01\x37\x00\x00\x00\x02Pix',b'\x00\x01\x50\x11w',b'\x00\x01\x50\x11h',b'\x00\x01\x50\x11d',b'\x00\x01\x50\x11spp',b'\x00\x01\x50\x11wpl',b'\x00\x01\x50\x11refcount',b'\x00\x00\x05\x11xres',b'\x00\x00\x05\x11yres',b'\x00\x00\x05\x11informat',b'\x00\x00\x05\x11special',b'\x00\x01\x3E\x11text',b'\x00\x01\x4C\x11colormap',b'\x00\x01\x4F\x11data'),(b'\x00\x00\x01\x39\x00\x00\x00\x02PixColormap',b'\x00\x01\x31\x11array',b'\x00\x00\x05\x11depth',b'\x00\x00\x05\x11nalloc',b'\x00\x00\x05\x11n'),(b'\x00\x00\x01\x38\x00\x00\x00\x02Pixa',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11nalloc',b'\x00\x01\x50\x11refcount',b'\x00\x00\x1A\x11pix',b'\x00\x00\x04\x11boxa'),(b'\x00\x00\x01\x3B\x00\x00\x00\x02Sarray',b'\x00\x00\x05\x11nalloc',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11refcount',b'\x00\x01\x3D\x11array'),(b'\x00\x00\x01\x3C\x00\x00\x00\x02Sel',b'\x00\x00\x05\x11sy',b'\x00\x00\x05\x11sx',b'\x00\x00\x05\x11cy',b'\x00\x00\x05\x11cx',b'\x00\x01\x48\x11data',b'\x00\x01\x3E\x11name')), - _enums = (b'\x00\x00\x01\x41\x00\x00\x00\x16$1\x00L_DEFAULT_ENCODE,L_JPEG_ENCODE,L_G4_ENCODE,L_FLATE_ENCODE,L_JP2K_ENCODE',b'\x00\x00\x01\x42\x00\x00\x00\x16$2\x00REMOVE_CMAP_TO_BINARY,REMOVE_CMAP_TO_GRAYSCALE,REMOVE_CMAP_TO_FULL_COLOR,REMOVE_CMAP_WITH_ALPHA,REMOVE_CMAP_BASED_ON_SRC',b'\x00\x00\x01\x43\x00\x00\x00\x16$3\x00L_NOCOPY,L_INSERT,L_COPY,L_CLONE,L_COPY_CLONE',b'\x00\x00\x01\x44\x00\x00\x00\x16$4\x00L_USE_WIDTHS,L_USE_WINDOWS',b'\x00\x00\x01\x45\x00\x00\x00\x16$5\x00L_BF_UNKNOWN,L_BF_ANY,L_BF_CODE128,L_BF_EAN8,L_BF_EAN13,L_BF_CODE2OF5,L_BF_CODEI2OF5,L_BF_CODE39,L_BF_CODE93,L_BF_CODABAR,L_BF_UPCA',b'\x00\x00\x01\x46\x00\x00\x00\x16$6\x00L_SEVERITY_EXTERNAL,L_SEVERITY_ALL,L_SEVERITY_DEBUG,L_SEVERITY_INFO,L_SEVERITY_WARNING,L_SEVERITY_ERROR,L_SEVERITY_NONE',b'\x00\x00\x01\x47\x00\x00\x00\x16$7\x00SEL_DONT_CARE,SEL_HIT,SEL_MISS'), - _typenames = (b'\x00\x00\x01\x33BOX',b'\x00\x00\x01\x34BOXA',b'\x00\x00\x01\x36L_COMP_DATA',b'\x00\x00\x01\x37PIX',b'\x00\x00\x01\x38PIXA',b'\x00\x00\x01\x39PIXCMAP',b'\x00\x00\x01\x3BSARRAY',b'\x00\x00\x01\x3CSEL',b'\x00\x00\x00\x32l_float32',b'\x00\x00\x01\x40l_float64',b'\x00\x00\x01\x4Al_int16',b'\x00\x00\x00\x05l_int32',b'\x00\x00\x01\x49l_int64',b'\x00\x00\x01\x4Bl_int8',b'\x00\x00\x00\x05l_ok',b'\x00\x00\x01\x52l_uint16',b'\x00\x00\x01\x50l_uint32',b'\x00\x00\x01\x51l_uint64',b'\x00\x00\x01\x4El_uint8'), + _types = b'\x00\x00\x01\x0D\x00\x01\x5C\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x5D\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x61\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x63\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x01\x62\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x09\x03\x00\x00\x18\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x5E\x03\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x01\x11\x00\x00\x01\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x18\x11\x00\x00\x05\x03\x00\x00\x11\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x01\x67\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x2A\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x2A\x11\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x00\x11\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x6A\x03\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x7C\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x09\x0D\x00\x01\x7E\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\x11\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x65\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x65\x03\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x65\x0D\x00\x00\x11\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xA4\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x4D\x0D\x00\x00\x92\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x92\x11\x00\x00\x00\x0F\x00\x00\x4D\x0D\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x69\x0D\x00\x00\x4D\x11\x00\x00\x00\x0F\x00\x01\x69\x0D\x00\x00\x00\x0F\x00\x00\x2A\x0D\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x1C\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x1C\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x04\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x3A\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x2A\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xD6\x11\x00\x00\xD6\x11\x00\x00\xD6\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\xD6\x11\x00\x00\xD6\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x2A\x11\x00\x00\x2A\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x2A\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x01\x5F\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xD6\x11\x00\x00\xD6\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x18\x11\x00\x00\x18\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x09\x11\x00\x01\x7D\x03\x00\x00\x96\x03\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x92\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x92\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\xFF\x11\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x92\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x01\x7B\x03\x00\x01\x17\x11\x00\x00\x09\x11\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x01\x2C\x11\x00\x01\x17\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x05\x0D\x00\x01\x2C\x11\x00\x01\x17\x11\x00\x00\x09\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x25\x11\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x04\x03\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\xFF\x11\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x18\x11\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x11\x03\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\xA4\x11\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x4D\x03\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x00\x92\x11\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x01\x81\x03\x00\x00\x00\x0F\x00\x01\x81\x0D\x00\x01\x53\x03\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x00\x0A\x09\x00\x01\x60\x03\x00\x00\x02\x09\x00\x00\x03\x09\x00\x00\x06\x09\x00\x00\x07\x09\x00\x00\x04\x09\x00\x01\x66\x03\x00\x00\x08\x09\x00\x00\x09\x09\x00\x01\x69\x03\x00\x01\x6A\x03\x00\x00\x02\x01\x00\x00\x0E\x01\x00\x00\x00\x0B\x00\x00\x01\x0B\x00\x00\x02\x0B\x00\x00\x03\x0B\x00\x00\x04\x0B\x00\x00\x05\x0B\x00\x00\x06\x0B\x00\x00\x2A\x03\x00\x00\x0B\x01\x00\x00\x05\x01\x00\x00\x03\x01\x00\x01\x64\x03\x00\x01\x79\x03\x00\x01\x7A\x03\x00\x00\x05\x09\x00\x01\x7C\x03\x00\x00\x04\x01\x00\x01\x7E\x03\x00\x00\x08\x01\x00\x00\x0C\x01\x00\x00\x06\x01\x00\x00\x00\x01', + _globals = (b'\xFF\xFF\xFF\x0BL_BF_ANY',1,b'\xFF\xFF\xFF\x0BL_BF_CODABAR',9,b'\xFF\xFF\xFF\x0BL_BF_CODE128',2,b'\xFF\xFF\xFF\x0BL_BF_CODE2OF5',5,b'\xFF\xFF\xFF\x0BL_BF_CODE39',7,b'\xFF\xFF\xFF\x0BL_BF_CODE93',8,b'\xFF\xFF\xFF\x0BL_BF_CODEI2OF5',6,b'\xFF\xFF\xFF\x0BL_BF_EAN13',4,b'\xFF\xFF\xFF\x0BL_BF_EAN8',3,b'\xFF\xFF\xFF\x0BL_BF_UNKNOWN',0,b'\xFF\xFF\xFF\x0BL_BF_UPCA',10,b'\xFF\xFF\xFF\x0BL_CLONE',2,b'\xFF\xFF\xFF\x0BL_COPY',1,b'\xFF\xFF\xFF\x0BL_COPY_CLONE',3,b'\xFF\xFF\xFF\x0BL_DEFAULT_ENCODE',0,b'\xFF\xFF\xFF\x0BL_FLATE_ENCODE',3,b'\xFF\xFF\xFF\x0BL_G4_ENCODE',2,b'\xFF\xFF\xFF\x0BL_INSERT',0,b'\xFF\xFF\xFF\x0BL_JP2K_ENCODE',4,b'\xFF\xFF\xFF\x0BL_JPEG_ENCODE',1,b'\xFF\xFF\xFF\x0BL_NOCOPY',0,b'\xFF\xFF\xFF\x0BL_SEVERITY_ALL',1,b'\xFF\xFF\xFF\x0BL_SEVERITY_DEBUG',2,b'\xFF\xFF\xFF\x0BL_SEVERITY_ERROR',5,b'\xFF\xFF\xFF\x0BL_SEVERITY_EXTERNAL',0,b'\xFF\xFF\xFF\x0BL_SEVERITY_INFO',3,b'\xFF\xFF\xFF\x0BL_SEVERITY_NONE',6,b'\xFF\xFF\xFF\x0BL_SEVERITY_WARNING',4,b'\xFF\xFF\xFF\x0BL_USE_WIDTHS',1,b'\xFF\xFF\xFF\x0BL_USE_WINDOWS',2,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_BASED_ON_SRC',4,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_BINARY',0,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_FULL_COLOR',2,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_TO_GRAYSCALE',1,b'\xFF\xFF\xFF\x0BREMOVE_CMAP_WITH_ALPHA',3,b'\xFF\xFF\xFF\x0BSEL_DONT_CARE',0,b'\xFF\xFF\xFF\x0BSEL_HIT',1,b'\xFF\xFF\xFF\x0BSEL_MISS',2,b'\x00\x00\x00\x23boxClone',0,b'\x00\x01\x3E\x23boxDestroy',0,b'\x00\x01\x41\x23boxaDestroy',0,b'\x00\x00\x03\x23boxaGetBox',0,b'\x00\x01\x19\x23getImpliedFileFormat',0,b'\x00\x00\xBE\x23getLeptonicaVersion',0,b'\x00\x01\x44\x23l_CIDataDestroy',0,b'\x00\x01\x1C\x23l_generateCIDataForPdf',0,b'\x00\x01\x59\x23leptSetStderrHandler',0,b'\x00\x01\x56\x23lept_free',0,b'\x00\x00\xC0\x23makePixelSumTab8',0,b'\x00\x00\x31\x23pixAnd',0,b'\x00\x00\x3E\x23pixBackgroundNorm',0,b'\x00\x00\x36\x23pixCleanBackgroundToWhite',0,b'\x00\x00\x22\x23pixClipRectangle',0,b'\x00\x01\x01\x23pixColorFraction',0,b'\x00\x00\x85\x23pixColorMagnitude',0,b'\x00\x00\x1F\x23pixConvertRGBToLuminance',0,b'\x00\x00\x7C\x23pixConvertTo8',0,b'\x00\x00\xD3\x23pixCorrelationBinary',0,b'\x00\x00\xED\x23pixCountPixels',0,b'\x00\x00\x98\x23pixDeserializeFromMemory',0,b'\x00\x00\x7C\x23pixDeskew',0,b'\x00\x01\x47\x23pixDestroy',0,b'\x00\x00\x4A\x23pixDilate',0,b'\x00\x00\x1F\x23pixEndianByteSwapNew',0,b'\x00\x00\xD8\x23pixEqual',0,b'\x00\x00\x4A\x23pixErode',0,b'\x00\x00\x9C\x23pixExtractBarcodes',0,b'\x00\x00\x08\x23pixFindPageForeground',0,b'\x00\x00\xE8\x23pixFindSkew',0,b'\x00\x00\x4F\x23pixGammaTRC',0,b'\x00\x00\x27\x23pixGenHalftoneMask',0,b'\x00\x00\xFA\x23pixGenerateCIData',0,b'\x00\x00\xDD\x23pixGetAverageMaskedRGB',0,b'\x00\x00\x56\x23pixGlobalNormRGB',0,b'\x00\x00\x4A\x23pixHMT',0,b'\x00\x00\x2D\x23pixInvert',0,b'\x00\x00\x15\x23pixLocateBarcodes',0,b'\x00\x00\x80\x23pixMaskOverColorPixels',0,b'\x00\x00\x5E\x23pixMaskedThreshOnBackgroundNorm',0,b'\x00\x00\xF2\x23pixNumSignificantGrayColors',0,b'\x00\x01\x0A\x23pixOtsuAdaptiveThreshold',0,b'\x00\x00\x6A\x23pixOtsuThreshOnBackgroundNorm',0,b'\x00\x00\xA0\x23pixProcessBarcodes',0,b'\x00\x00\x91\x23pixRead',0,b'\x00\x00\xA7\x23pixReadBarcodes',0,b'\x00\x00\x94\x23pixReadMem',0,b'\x00\x00\x1B\x23pixReadStream',0,b'\x00\x00\x7C\x23pixRemoveColormap',0,b'\x00\x00\x80\x23pixRemoveColormapGeneral',0,b'\x00\x00\xCD\x23pixRenderBoxa',0,b'\x00\x00\x2D\x23pixRotate180',0,b'\x00\x00\x7C\x23pixRotateOrth',0,b'\x00\x00\x77\x23pixScale',0,b'\x00\x01\x14\x23pixSerializeToMemory',0,b'\x00\x00\x31\x23pixSubtract',0,b'\x00\x01\x22\x23pixWriteImpliedFormat',0,b'\x00\x01\x31\x23pixWriteMem',0,b'\x00\x01\x37\x23pixWriteMemJpeg',0,b'\x00\x01\x2B\x23pixWriteMemPng',0,b'\x00\x00\xC2\x23pixWriteStream',0,b'\x00\x00\xC7\x23pixWriteStreamJpeg',0,b'\x00\x01\x4A\x23pixaDestroy',0,b'\x00\x00\x10\x23pixaGetBox',0,b'\x00\x00\x8C\x23pixaGetPix',0,b'\x00\x01\x4D\x23sarrayDestroy',0,b'\x00\x00\xB4\x23selCreateBrick',0,b'\x00\x00\xAE\x23selCreateFromString',0,b'\x00\x01\x50\x23selDestroy',0,b'\x00\x00\xBB\x23selPrintToString',0,b'\x00\x01\x28\x23setMsgSeverity',0), + _struct_unions = ((b'\x00\x00\x01\x5C\x00\x00\x00\x02Box',b'\x00\x00\x05\x11x',b'\x00\x00\x05\x11y',b'\x00\x00\x05\x11w',b'\x00\x00\x05\x11h',b'\x00\x01\x7E\x11refcount'),(b'\x00\x00\x01\x5D\x00\x00\x00\x02Boxa',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11nalloc',b'\x00\x01\x7E\x11refcount',b'\x00\x00\x25\x11box'),(b'\x00\x00\x01\x60\x00\x00\x00\x02L_Compressed_Data',b'\x00\x00\x05\x11type',b'\x00\x01\x7B\x11datacomp',b'\x00\x00\x96\x11nbytescomp',b'\x00\x01\x69\x11data85',b'\x00\x00\x96\x11nbytes85',b'\x00\x01\x69\x11cmapdata85',b'\x00\x01\x69\x11cmapdatahex',b'\x00\x00\x05\x11ncolors',b'\x00\x00\x05\x11w',b'\x00\x00\x05\x11h',b'\x00\x00\x05\x11bps',b'\x00\x00\x05\x11spp',b'\x00\x00\x05\x11minisblack',b'\x00\x00\x05\x11predictor',b'\x00\x00\x96\x11nbytes',b'\x00\x00\x05\x11res'),(b'\x00\x00\x01\x61\x00\x00\x00\x02Pix',b'\x00\x01\x7E\x11w',b'\x00\x01\x7E\x11h',b'\x00\x01\x7E\x11d',b'\x00\x01\x7E\x11spp',b'\x00\x01\x7E\x11wpl',b'\x00\x01\x7E\x11refcount',b'\x00\x00\x05\x11xres',b'\x00\x00\x05\x11yres',b'\x00\x00\x05\x11informat',b'\x00\x00\x05\x11special',b'\x00\x01\x69\x11text',b'\x00\x01\x77\x11colormap',b'\x00\x01\x7D\x11data'),(b'\x00\x00\x01\x64\x00\x00\x00\x02PixColormap',b'\x00\x01\x57\x11array',b'\x00\x00\x05\x11depth',b'\x00\x00\x05\x11nalloc',b'\x00\x00\x05\x11n'),(b'\x00\x00\x01\x7A\x00\x00\x00\x10PixComp',),(b'\x00\x00\x01\x62\x00\x00\x00\x02Pixa',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11nalloc',b'\x00\x01\x7E\x11refcount',b'\x00\x00\x18\x11pix',b'\x00\x00\x04\x11boxa'),(b'\x00\x00\x01\x63\x00\x00\x00\x02PixaComp',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11nalloc',b'\x00\x00\x05\x11offset',b'\x00\x01\x78\x11pixc',b'\x00\x00\x04\x11boxa'),(b'\x00\x00\x01\x66\x00\x00\x00\x02Sarray',b'\x00\x00\x05\x11nalloc',b'\x00\x00\x05\x11n',b'\x00\x00\x05\x11refcount',b'\x00\x01\x68\x11array'),(b'\x00\x00\x01\x67\x00\x00\x00\x02Sel',b'\x00\x00\x05\x11sy',b'\x00\x00\x05\x11sx',b'\x00\x00\x05\x11cy',b'\x00\x00\x05\x11cx',b'\x00\x01\x73\x11data',b'\x00\x01\x69\x11name'),(b'\x00\x00\x01\x5E\x00\x00\x00\x10_IO_FILE',)), + _enums = (b'\x00\x00\x01\x6C\x00\x00\x00\x16$1\x00L_DEFAULT_ENCODE,L_JPEG_ENCODE,L_G4_ENCODE,L_FLATE_ENCODE,L_JP2K_ENCODE',b'\x00\x00\x01\x6D\x00\x00\x00\x16$2\x00REMOVE_CMAP_TO_BINARY,REMOVE_CMAP_TO_GRAYSCALE,REMOVE_CMAP_TO_FULL_COLOR,REMOVE_CMAP_WITH_ALPHA,REMOVE_CMAP_BASED_ON_SRC',b'\x00\x00\x01\x6E\x00\x00\x00\x16$3\x00L_NOCOPY,L_INSERT,L_COPY,L_CLONE,L_COPY_CLONE',b'\x00\x00\x01\x6F\x00\x00\x00\x16$4\x00L_USE_WIDTHS,L_USE_WINDOWS',b'\x00\x00\x01\x70\x00\x00\x00\x16$5\x00L_BF_UNKNOWN,L_BF_ANY,L_BF_CODE128,L_BF_EAN8,L_BF_EAN13,L_BF_CODE2OF5,L_BF_CODEI2OF5,L_BF_CODE39,L_BF_CODE93,L_BF_CODABAR,L_BF_UPCA',b'\x00\x00\x01\x71\x00\x00\x00\x16$6\x00L_SEVERITY_EXTERNAL,L_SEVERITY_ALL,L_SEVERITY_DEBUG,L_SEVERITY_INFO,L_SEVERITY_WARNING,L_SEVERITY_ERROR,L_SEVERITY_NONE',b'\x00\x00\x01\x72\x00\x00\x00\x16$7\x00SEL_DONT_CARE,SEL_HIT,SEL_MISS'), + _typenames = (b'\x00\x00\x01\x5CBOX',b'\x00\x00\x01\x5DBOXA',b'\x00\x00\x01\x5EFILE',b'\x00\x00\x01\x60L_COMP_DATA',b'\x00\x00\x01\x61PIX',b'\x00\x00\x01\x62PIXA',b'\x00\x00\x01\x63PIXAC',b'\x00\x00\x01\x64PIXCMAP',b'\x00\x00\x01\x66SARRAY',b'\x00\x00\x01\x67SEL',b'\x00\x00\x00\x3Al_float32',b'\x00\x00\x01\x6Bl_float64',b'\x00\x00\x01\x75l_int16',b'\x00\x00\x00\x05l_int32',b'\x00\x00\x01\x74l_int64',b'\x00\x00\x01\x76l_int8',b'\x00\x00\x00\x05l_ok',b'\x00\x00\x01\x80l_uint16',b'\x00\x00\x01\x7El_uint32',b'\x00\x00\x01\x7Fl_uint64',b'\x00\x00\x01\x7Cl_uint8'), ) diff --git a/src/ocrmypdf/lib/compile_leptonica.py b/src/ocrmypdf/lib/compile_leptonica.py index c76dd324..0e3afb56 100644 --- a/src/ocrmypdf/lib/compile_leptonica.py +++ b/src/ocrmypdf/lib/compile_leptonica.py @@ -1,20 +1,12 @@ #!/usr/bin/env python3 # © 2017 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from pathlib import Path from cffi import FFI @@ -74,6 +66,17 @@ struct Pixa }; typedef struct Pixa PIXA; +/*! Array of compressed pix */ +struct PixaComp +{ + l_int32 n; /*!< number of PixComp in ptr array */ + l_int32 nalloc; /*!< number of PixComp ptrs allocated */ + l_int32 offset; /*!< indexing offset into ptr array */ + struct PixComp **pixc; /*!< the array of ptrs to PixComp */ + struct Boxa *boxa; /*!< array of boxes */ +}; +typedef struct PixaComp PIXAC; + struct Box { l_int32 x; @@ -210,9 +213,15 @@ ffibuilder.cdef( """ PIX * pixRead ( const char *filename ); PIX * pixReadMem ( const l_uint8 *data, size_t size ); +PIX * pixReadStream ( FILE *fp, l_int32 hint ); PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley ); l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf ); l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive ); +l_int32 getImpliedFileFormat ( const char *filename ); +l_ok pixWriteStream ( FILE *fp, PIX *pix, l_int32 format ); +l_ok pixWriteStreamJpeg ( FILE *fp, PIX *pixs, l_int32 quality, l_int32 progressive ); +l_ok pixWriteMem ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 format ); +l_ok pixWriteMemJpeg ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 quality, l_int32 progressive ); l_int32 pixWriteMemPng(l_uint8 **pdata, size_t *psize, @@ -294,14 +303,12 @@ pixCleanBackgroundToWhite(PIX *pixs, l_int32 whiteval); BOX * -pixFindPageForeground(PIX *pixs, - l_int32 threshold, - l_int32 mindist, - l_int32 erasedist, - l_int32 pagenum, - l_int32 showmorph, - l_int32 display, - const char *pdfdir); +pixFindPageForeground ( PIX *pixs, + l_int32 threshold, + l_int32 mindist, + l_int32 erasedist, + l_int32 showmorph, + PIXAC *pixac ); PIX * pixClipRectangle(PIX *pixs, @@ -414,7 +421,10 @@ pixExtractBarcodes(PIX *pixs, l_int32 debugflag); BOXA * -pixLocateBarcodes ( PIX *pixs, l_int32 thresh, PIX **ppixb, PIX **ppixm ); +pixLocateBarcodes ( PIX *pixs, + l_int32 thresh, + PIX **ppixb, + PIX **ppixm ); SARRAY * pixReadBarcodes(PIXA *pixa, @@ -423,6 +433,12 @@ pixReadBarcodes(PIXA *pixa, SARRAY **psaw, l_int32 debugflag); +PIX * +pixGenHalftoneMask(PIX *pixs, + PIX **ppixtext, + l_int32 *phtfound, + PIXA *pixadb); + l_int32 l_generateCIDataForPdf(const char *fname, PIX *pix, @@ -483,6 +499,8 @@ void selDestroy ( SEL **psel ); l_int32 setMsgSeverity(l_int32 newsev); +void +leptSetStderrHandler(void (*handler)(const char *)); """ ) @@ -491,3 +509,8 @@ ffibuilder.set_source("ocrmypdf.lib._leptonica", None) if __name__ == '__main__': ffibuilder.compile(verbose=True) + if Path('ocrmypdf/lib/_leptonica.py').exists() and Path('src/ocrmypdf').exists(): + output = Path('ocrmypdf/lib/_leptonica.py') + output.rename('src/ocrmypdf/lib/_leptonica.py') + Path('ocrmypdf/lib').rmdir() + Path('ocrmypdf').rmdir() diff --git a/src/ocrmypdf/optimize.py b/src/ocrmypdf/optimize.py index 4ab6920c..fce66464 100644 --- a/src/ocrmypdf/optimize.py +++ b/src/ocrmypdf/optimize.py @@ -1,108 +1,161 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import concurrent.futures import logging import sys +import tempfile from collections import defaultdict from os import fspath from pathlib import Path +from typing import ( + Callable, + Dict, + Iterator, + List, + MutableSet, + NamedTuple, + NewType, + Optional, + Sequence, + Tuple, +) +import img2pdf +import pikepdf +from pikepdf import Dictionary, Name, Object, Pdf, PdfImage from PIL import Image -import pikepdf -from pikepdf import Name, Dictionary, Array +from ocrmypdf import leptonica +from ocrmypdf._concurrent import Executor, SerialExecutor +from ocrmypdf._exec import jbig2enc, pngquant +from ocrmypdf._jobcontext import PdfContext +from ocrmypdf.exceptions import OutputFileAccessError +from ocrmypdf.helpers import safe_symlink -from . import leptonica -from ._jobcontext import JobContext -from .exec import jbig2enc, pngquant -from .helpers import re_symlink +log = logging.getLogger(__name__) DEFAULT_JPEG_QUALITY = 75 DEFAULT_PNG_QUALITY = 70 -def img_name(root, xref, ext): - return fspath(root / f'{xref:08d}{ext}') +Xref = NewType('Xref', int) -def png_name(root, xref): +class XrefExt(NamedTuple): # pylint: disable=inherit-non-class + xref: Xref + ext: str + + +def img_name(root: Path, xref: Xref, ext: str) -> Path: + return root / f'{xref:08d}{ext}' + + +def png_name(root: Path, xref: Xref) -> Path: return img_name(root, xref, '.png') -def jpg_name(root, xref): +def jpg_name(root: Path, xref: Xref) -> Path: return img_name(root, xref, '.jpg') -def tif_name(root, xref): - return img_name(root, xref, '.tif') +def extract_image_filter( + pike: Pdf, root: Path, image: Object, xref: Xref +) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]: + del pike # unused args + del root - -def extract_image_filter(pike, root, log, image, xref): if image.Subtype != Name.Image: return None if image.Length < 100: - log.debug("Skipping small image, xref %s", xref) + log.debug(f"Skipping small image, xref {xref}") + return None + if image.Width < 8 or image.Height < 8: # Issue 732 + log.debug(f"Skipping oddly sized image, xref {xref}") return None - pim = pikepdf.PdfImage(image) + pim = PdfImage(image) if len(pim.filter_decodeparms) > 1: - log.debug("Skipping multiply filtered, xref %s", xref) + log.debug(f"Skipping multiply filtered image, xref {xref}") return None filtdp = pim.filter_decodeparms[0] if pim.bits_per_component > 8: + log.debug(f"Skipping wide gamut image, xref {xref}") return None # Don't mess with wide gamut images if filtdp[0] == Name.JPXDecode: + log.debug(f"Skipping JPEG2000 iamge, xref {xref}") return None # Don't do JPEG2000 + if filtdp[0] == Name.CCITTFaxDecode and filtdp[1].get('/K', 0) >= 0: + log.debug(f"Skipping CCITT Group 3 image, xref {xref}") + return None # pikepdf doesn't support Group 3 yet + + if Name.Decode in image: + log.debug(f"Skipping image with Decode table, xref {xref}") + return None # Don't mess with custom Decode tables + return pim, filtdp -def extract_image_jbig2(*, pike, root, log, image, xref, options): - result = extract_image_filter(pike, root, log, image, xref) +def extract_image_jbig2( + *, pike: pikepdf.Pdf, root: Path, image: Object, xref: Xref, options +) -> Optional[XrefExt]: + del options # unused arg + + result = extract_image_filter(pike, root, image, xref) if result is None: return None pim, filtdp = result if ( pim.bits_per_component == 1 - and filtdp != Name.JBIG2Decode + and filtdp[0] != Name.JBIG2Decode and jbig2enc.available() ): - try: - imgname = Path(root / f'{xref:08d}') - with imgname.open('wb') as f: - ext = pim.extract_to(stream=f) - imgname.rename(imgname.with_suffix(ext)) - except pikepdf.UnsupportedImageTypeError: - return None - return xref, ext + # Save any colorspace associated with the image, so that we + # will export a pure 1-bit PNG with no palette or ICC profile. + # Showing the palette or ICC to jbig2enc will cause it to perform + # colorspace transform to 1bpp, which will conflict the palette or + # ICC if it exists. + colorspace = pim.obj.get(pikepdf.Name.ColorSpace, None) + if colorspace is not None or pim.image_mask: + try: + # Set to DeviceGray temporarily; we already in 1 bpc. + pim.obj.ColorSpace = pikepdf.Name.DeviceGray + imgname = root / f'{xref:08d}' + with imgname.open('wb') as f: + ext = pim.extract_to(stream=f) + imgname.rename(imgname.with_suffix(ext)) + except pikepdf.UnsupportedImageTypeError: + return None + finally: + # Restore image colorspace after temporarily setting it to DeviceGray + if colorspace is not None: + pim.obj.ColorSpace = colorspace + else: + del pim.obj.ColorSpace + return XrefExt(xref, ext) return None -def extract_image_generic(*, pike, root, log, image, xref, options): - result = extract_image_filter(pike, root, log, image, xref) +def extract_image_generic( + *, pike: Pdf, root: Path, image: PdfImage, xref: Xref, options +) -> Optional[XrefExt]: + result = extract_image_filter(pike, root, image, xref) if result is None: return None pim, filtdp = result + # Don't try to PNG-optimize 1bpp images, since JBIG2 does it better. + if pim.bits_per_component == 1: + return None + if filtdp[0] == Name.DCTDecode and options.optimize >= 2: # This is a simple heuristic derived from some training data, that has # about a 70% chance of guessing whether the JPEG is high quality, @@ -121,13 +174,13 @@ def extract_image_generic(*, pike, root, log, image, xref, options): # with Image.open(stream) as im: # im.save(jpg_name(root, xref), icc_profile=iccbytes) try: - imgname = Path(root / f'{xref:08d}') + imgname = root / f'{xref:08d}' with imgname.open('wb') as f: ext = pim.extract_to(stream=f) imgname.rename(imgname.with_suffix(ext)) except pikepdf.UnsupportedImageTypeError: return None - return xref, ext + return XrefExt(xref, ext) elif ( pim.indexed and pim.colorspace in pim.SIMPLE_COLORSPACES @@ -136,17 +189,33 @@ def extract_image_generic(*, pike, root, log, image, xref, options): # Try to improve on indexed images - these are far from low hanging # fruit in most cases pim.as_pil_image().save(png_name(root, xref)) - return xref, '.png' + return XrefExt(xref, '.png') elif not pim.indexed and pim.colorspace in pim.SIMPLE_COLORSPACES: # An optimization opportunity here, not currently taken, is directly # generating a PNG from compressed data pim.as_pil_image().save(png_name(root, xref)) - return xref, '.png' + return XrefExt(xref, '.png') + elif ( + not pim.indexed + and pim.colorspace == Name.ICCBased + and pim.bits_per_component == 1 + and not options.jbig2_lossy + ): + # We can losslessly optimize 1-bit images to CCITT or JBIG2 without + # paying any attention to the ICC profile, provided we're not doing + # lossy JBIG2 + pim.as_pil_image().save(png_name(root, xref)) + return XrefExt(xref, '.png') return None -def extract_images(pike, root, log, options, extract_fn): +def extract_images( + pike: Pdf, + root: Path, + options, + extract_fn: Callable[..., Optional[XrefExt]], +) -> Iterator[Tuple[int, XrefExt]]: """Extract image using extract_fn Enumerate images on each page, lookup their xref/ID number in the PDF. @@ -162,8 +231,8 @@ def extract_images(pike, root, log, options, extract_fn): extension. extract_fn must also extract the file it finds interesting. """ - include_xrefs = set() - exclude_xrefs = set() + include_xrefs: MutableSet[Xref] = set() + exclude_xrefs: MutableSet[Xref] = set() pageno_for_xref = {} errors = 0 for pageno, page in enumerate(pike.pages): @@ -174,12 +243,14 @@ def extract_images(pike, root, log, options, extract_fn): for _imname, image in dict(xobjs).items(): if image.objgen[1] != 0: continue # Ignore images in an incremental PDF - xref = image.objgen[0] + xref = Xref(image.objgen[0]) if hasattr(image, 'SMask'): # Ignore soft masks - smask_xref = image.SMask.objgen[0] + smask_xref = Xref(image.SMask.objgen[0]) exclude_xrefs.add(smask_xref) + log.debug(f"Skipping image {smask_xref} because it is an SMask") include_xrefs.add(xref) + log.debug(f"Treating {xref} as an optimization candidate") if xref not in pageno_for_xref: pageno_for_xref[xref] = pageno @@ -188,91 +259,100 @@ def extract_images(pike, root, log, options, extract_fn): image = pike.get_object((xref, 0)) try: result = extract_fn( - pike=pike, root=root, log=log, image=image, xref=xref, options=options + pike=pike, root=root, image=image, xref=xref, options=options ) - except Exception as e: - log.debug("Image xref %s, error %s", xref, repr(e)) + except Exception: # pylint: disable=broad-except + log.exception(f"While extracting image xref {xref}, an error occurred") errors += 1 else: if result: _, ext = result - yield pageno_for_xref[xref], xref, ext + yield pageno_for_xref[xref], XrefExt(xref, ext) -def extract_images_generic(pike, root, log, options): +def extract_images_generic( + pike: Pdf, root: Path, options +) -> Tuple[List[Xref], List[Xref]]: """Extract any >=2bpp image we think we can improve""" jpegs = [] pngs = [] - for _, xref, ext in extract_images(pike, root, log, options, extract_image_generic): - log.debug('xref = %s ext = %s', xref, ext) - if ext == '.png': - pngs.append(xref) - elif ext == '.jpg': - jpegs.append(xref) + for _, xref_ext in extract_images(pike, root, options, extract_image_generic): + log.debug('%s', xref_ext) + if xref_ext.ext == '.png': + pngs.append(xref_ext.xref) + elif xref_ext.ext == '.jpg': + jpegs.append(xref_ext.xref) log.debug("Optimizable images: JPEGs: %s PNGs: %s", len(jpegs), len(pngs)) return jpegs, pngs -def extract_images_jbig2(pike, root, log, options): +def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefExt]]: """Extract any bitonal image that we think we can improve as JBIG2""" jbig2_groups = defaultdict(list) - for pageno, xref, ext in extract_images( - pike, root, log, options, extract_image_jbig2 - ): + for pageno, xref_ext in extract_images(pike, root, options, extract_image_jbig2): group = pageno // options.jbig2_page_group_size - jbig2_groups[group].append((xref, ext)) + jbig2_groups[group].append(xref_ext) - # Elide empty groups - jbig2_groups = { - group: xrefs for group, xrefs in jbig2_groups.items() if len(xrefs) > 0 - } log.debug("Optimizable images: JBIG2 groups: %s", (len(jbig2_groups),)) return jbig2_groups -def _produce_jbig2_images(jbig2_groups, root, log, options): +def _produce_jbig2_images( + jbig2_groups: Dict[int, List[XrefExt]], root: Path, options, executor: Executor +) -> None: """Produce JBIG2 images from their groups""" - def jbig2_group_futures(executor, root, groups): + def jbig2_group_args(root: Path, groups: Dict[int, List[XrefExt]]): for group, xref_exts in groups.items(): prefix = f'group{group:08d}' - future = executor.submit( - jbig2enc.convert_group, - cwd=fspath(root), - infiles=(img_name(root, xref, ext) for xref, ext in xref_exts), - out_prefix=prefix, + yield ( + fspath(root), # =cwd + (img_name(root, xref, ext) for xref, ext in xref_exts), # =infiles + prefix, # =out_prefix ) - yield future - def jbig2_single_futures(executor, root, groups): + def jbig2_single_args(root, groups: Dict[int, List[XrefExt]]): for group, xref_exts in groups.items(): prefix = f'group{group:08d}' # Second loop is to ensure multiple images per page are unpacked for n, xref_ext in enumerate(xref_exts): xref, ext = xref_ext - future = executor.submit( - jbig2enc.convert_single, - cwd=fspath(root), - infile=img_name(root, xref, ext), - outfile=root / f'{prefix}.{n:04d}', + yield ( + fspath(root), + img_name(root, xref, ext), + root / f'{prefix}.{n:04d}', ) - yield future if options.jbig2_page_group_size > 1: - jbig2_futures = jbig2_group_futures + jbig2_args = jbig2_group_args + jbig2_convert = jbig2enc.convert_group_mp else: - jbig2_futures = jbig2_single_futures + jbig2_args = jbig2_single_args + jbig2_convert = jbig2enc.convert_single_mp - with concurrent.futures.ThreadPoolExecutor(max_workers=options.jobs) as executor: - futures = jbig2_futures(executor, root, jbig2_groups) - for future in concurrent.futures.as_completed(futures): - proc = future.result() - log.debug(proc.stderr.decode()) + executor( + use_threads=True, + max_workers=options.jobs, + tqdm_kwargs=dict( + total=len(jbig2_groups), + desc="JBIG2", + unit='item', + disable=not options.progress_bar, + ), + task=jbig2_convert, + task_arguments=jbig2_args(root, jbig2_groups), + ) -def convert_to_jbig2(pike, jbig2_groups, root, log, options): +def convert_to_jbig2( + pike: Pdf, + jbig2_groups: Dict[int, List[XrefExt]], + root: Path, + options, + executor: Executor, +) -> None: """Convert images to JBIG2 and insert into PDF. When the JBIG2 page group size is > 1 we do several JBIG2 images at once @@ -286,7 +366,7 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options): and needs no dictionary. Currently this must be lossless JBIG2. """ - _produce_jbig2_images(jbig2_groups, root, log, options) + _produce_jbig2_images(jbig2_groups, root, options, executor) for group, xref_exts in jbig2_groups.items(): prefix = f'group{group:08d}' @@ -310,128 +390,150 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options): ) -def transcode_jpegs(pike, jpegs, root, log, options): - for xref in jpegs: - in_jpg = Path(jpg_name(root, xref)) - opt_jpg = in_jpg.with_suffix('.opt.jpg') +def _optimize_jpeg(args): + xref, in_jpg, opt_jpg, jpeg_quality = args - # This produces a debug warning from PIL - # DEBUG:PIL.Image:Error closing: 'NoneType' object has no attribute - # 'close'. Seems to be mostly harmless - # https://github.com/python-pillow/Pillow/issues/1144 - with Image.open(fspath(in_jpg)) as im: - im.save(fspath(opt_jpg), optimize=True, quality=options.jpeg_quality) + # This may produce a debug warning from PIL + # DEBUG:PIL.Image:Error closing: 'NoneType' object has no attribute + # 'close'. Seems to be mostly harmless + # https://github.com/python-pillow/Pillow/issues/1144 + with Image.open(in_jpg) as im: + im.save(opt_jpg, optimize=True, quality=jpeg_quality) - if opt_jpg.stat().st_size > in_jpg.stat().st_size: - log.debug("xref %s, jpeg, made larger - skip", xref) - continue + if opt_jpg.stat().st_size > in_jpg.stat().st_size: + log.debug("xref %s, jpeg, made larger - skip", xref) + opt_jpg.unlink() + opt_jpg = None + return xref, opt_jpg + + +def transcode_jpegs( + pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor +) -> None: + def jpeg_args(): + for xref in jpegs: + in_jpg = jpg_name(root, xref) + opt_jpg = in_jpg.with_suffix('.opt.jpg') + yield xref, in_jpg, opt_jpg, options.jpeg_quality + + def finish_jpeg(result, pbar): + xref, opt_jpg = result + if opt_jpg: + compdata = leptonica.CompressedData.open(opt_jpg) + im_obj = pike.get_object(xref, 0) + im_obj.write(compdata.read(), filter=Name.DCTDecode) + pbar.update() + + executor( + use_threads=True, # Processes are significantly slower at this task + max_workers=options.jobs, + tqdm_kwargs=dict( + desc="JPEGs", + total=len(jpegs), + unit='image', + disable=not options.progress_bar, + ), + task=_optimize_jpeg, + task_arguments=jpeg_args(), + task_finished=finish_jpeg, + ) + + +def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool: + output = filename.with_suffix('.png.pdf') + with output.open('wb') as f: + img2pdf.convert(fspath(filename), outputstream=f) + + with pikepdf.open(output) as pdf_image: + foreign_image = next(pdf_image.pages[0].images.values()) + local_image = pike.copy_foreign(foreign_image) - compdata = leptonica.CompressedData.open(opt_jpg) im_obj = pike.get_object(xref, 0) - im_obj.write(compdata.read(), filter=Name.DCTDecode) + im_obj.write( + local_image.read_raw_bytes(), + filter=local_image.Filter, + decode_parms=local_image.DecodeParms, + ) + + # Don't copy keys from the new image... + del_keys = set(im_obj.keys()) - set(local_image.keys()) + # ...except for the keep_fields, which are essential to displaying + # the image correctly and preserving its metadata. (/Decode arrays + # and /SMaskInData are implicitly discarded prior to this point.) + keep_fields = { + '/ID', + '/Intent', + '/Interpolate', + '/Mask', + '/Metadata', + '/OC', + '/OPI', + '/SMask', + '/StructParent', + } + del_keys -= keep_fields + for key in local_image.keys(): + if key != Name.Length and str(key) not in keep_fields: + im_obj[key] = local_image[key] + for key in del_keys: + del im_obj[key] + return True -def transcode_pngs(pike, images, image_name_fn, root, log, options): +def transcode_pngs( + pike: Pdf, + images: Sequence[Xref], + image_name_fn: Callable[[Path, Xref], Path], + root: Path, + options, + executor, +) -> None: + modified: MutableSet[Xref] = set() if options.optimize >= 2: png_quality = ( max(10, options.png_quality - 10), min(100, options.png_quality + 10), ) - with concurrent.futures.ThreadPoolExecutor( - max_workers=options.jobs - ) as executor: + + def pngquant_args(): for xref in images: log.debug(image_name_fn(root, xref)) - executor.submit( - pngquant.quantize, + yield ( image_name_fn(root, xref), png_name(root, xref), png_quality[0], png_quality[1], ) + modified.add(xref) - for xref in images: - im_obj = pike.get_object(xref, 0) - try: - compdata = leptonica.CompressedData.open(png_name(root, xref)) - except leptonica.LeptonicaError as e: - # Most likely this means file not found, i.e. quantize did not - # produce an improved version - log.error(e) - continue + executor( + use_threads=True, + max_workers=options.jobs, + tqdm_kwargs=dict( + desc="PNGs", + total=len(images), + unit='image', + disable=not options.progress_bar, + ), + task=pngquant.quantize_mp, + task_arguments=pngquant_args(), + ) - # If re-coded image is larger don't use it - we test here because - # pngquant knows the size of the temporary output file but not the actual - # object in the PDF - if len(compdata) > int(im_obj.stream_dict.Length): - log.debug( - f"pngquant: pngquant did not improve over original image " - f"{len(compdata)} > {int(im_obj.stream_dict.Length)}" - ) - continue - - # When a PNG is inserted into a PDF, we more or less copy the IDAT section from - # the PDF and transfer the rest of the PNG headers to PDF image metadata. - # One thing we have to do is tell the PDF reader whether a predictor was used - # on the image before Flate encoding. (Typically one is.) - # According to Leptonica source, PDF readers don't actually need us - # to specify the correct predictor, they just need a value of either: - # 1 - no predictor - # 10-14 - there is a predictor - # Leptonica's compdata->predictor only tells TRUE or FALSE - # From there the PNG decoder can infer the rest from the file. - # In practice the predictor should be Paeth, 14, so we'll use that. - # See: - # - PDF RM 7.4.4.4 Table 10 - # - https://github.com/DanBloomberg/leptonica/blob/master/src/pdfio2.c#L757 - predictor = 14 if compdata.predictor > 0 else 1 - dparms = Dictionary(Predictor=predictor) - if predictor > 1: - dparms.BitsPerComponent = compdata.bps # Yes, this is redundant - dparms.Colors = compdata.spp - dparms.Columns = compdata.w - - im_obj.BitsPerComponent = compdata.bps - im_obj.Width = compdata.w - im_obj.Height = compdata.h - - if compdata.ncolors > 0: - # .ncolors is the number of colors in the palette, not the number of - # colors used in a true color image - palette_pdf_string = compdata.get_palette_pdf_string() - palette_data = pikepdf.Object.parse(palette_pdf_string) - palette_stream = pikepdf.Stream(pike, bytes(palette_data)) - palette = [ - Name.Indexed, - Name.DeviceRGB, - compdata.ncolors - 1, - palette_stream, - ] - cs = palette - else: - if compdata.spp == 1: - # PDF interprets binary-1 as black in 1bpp, but PNG sets - # black to 0 for 1bpp. Create a palette that informs the PDF - # of the mapping - seems cleaner to go this way but pikepdf - # needs to be patched to support it. - # palette = [Name.Indexed, Name.DeviceGray, 1, b"\xff\x00"] - # cs = palette - cs = Name.DeviceGray - elif compdata.spp == 3: - cs = Name.DeviceRGB - elif compdata.spp == 4: - cs = Name.DeviceCMYK - if compdata.bps == 1: - im_obj.Decode = [1, 0] # Bit of a kludge but this inverts photometric too - im_obj.ColorSpace = cs - im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms) + for xref in modified: + filename = png_name(root, xref) + _transcode_png(pike, filename, xref) -def optimize(input_file, output_file, log, context): - - options = context.get_options() +def optimize( + input_file: Path, + output_file: Path, + context, + save_settings, + executor: Executor = SerialExecutor(), +) -> None: + options = context.options if options.optimize == 0: - re_symlink(input_file, output_file, log) + safe_symlink(input_file, output_file) return if options.jpeg_quality == 0: @@ -441,73 +543,88 @@ def optimize(input_file, output_file, log, context): if options.jbig2_page_group_size == 0: options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1 - pike = pikepdf.Pdf.open(input_file) + with pikepdf.Pdf.open(input_file) as pike: + root = output_file.parent / 'images' + root.mkdir(exist_ok=True) - root = Path(output_file).parent / 'images' - root.mkdir(exist_ok=True) + jpegs, pngs = extract_images_generic(pike, root, options) + transcode_jpegs(pike, jpegs, root, options, executor) + # if options.optimize >= 2: + # Try pngifying the jpegs + # transcode_pngs(pike, jpegs, jpg_name, root, options) + transcode_pngs(pike, pngs, png_name, root, options, executor) - jpegs, pngs = extract_images_generic(pike, root, log, options) - transcode_jpegs(pike, jpegs, root, log, options) - # if options.optimize >= 2: - # Try pngifying the jpegs - # transcode_pngs(pike, jpegs, jpg_name, root, log, options) - transcode_pngs(pike, pngs, png_name, root, log, options) + jbig2_groups = extract_images_jbig2(pike, root, options) + convert_to_jbig2(pike, jbig2_groups, root, options, executor) - jbig2_groups = extract_images_jbig2(pike, root, log, options) - convert_to_jbig2(pike, jbig2_groups, root, log, options) + target_file = output_file.with_suffix('.opt.pdf') + pike.remove_unreferenced_resources() + pike.save(target_file, **save_settings) - target_file = Path(output_file).with_suffix('.opt.pdf') - pike.remove_unreferenced_resources() - pike.save( - target_file, - preserve_pdfa=True, - object_stream_mode=pikepdf.ObjectStreamMode.generate, - ) - - input_size = Path(input_file).stat().st_size - output_size = Path(target_file).stat().st_size + input_size = input_file.stat().st_size + output_size = target_file.stat().st_size + if output_size == 0: + raise OutputFileAccessError( + f"Output file not created after optimizing. We probably ran " + f"out of disk space in the temporary folder: {tempfile.gettempdir()}." + ) ratio = input_size / output_size savings = 1 - output_size / input_size - log.info(f"Optimize ratio: {ratio:.2f} savings: {(100 * savings):.1f}%") + log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}") if savings < 0: - log.info("Optimize did not improve the file - discarded") - re_symlink(input_file, output_file, log) + log.info("Image optimization did not improve the file - discarded") + # We still need to save the file + with pikepdf.open(input_file) as pike: + pike.remove_unreferenced_resources() + pike.save(output_file, **save_settings) else: - re_symlink(target_file, output_file, log) + safe_symlink(target_file, output_file) def main(infile, outfile, level, jobs=1): - from tempfile import TemporaryDirectory - from shutil import copy + from shutil import copy # pylint: disable=import-outside-toplevel + from tempfile import TemporaryDirectory # pylint: disable=import-outside-toplevel class OptimizeOptions: """Emulate ocrmypdf's options""" - def __init__(self, jobs, optimize, jpeg_quality, png_quality, jb2lossy): + def __init__( + self, input_file, jobs, optimize_, jpeg_quality, png_quality, jb2lossy + ): + self.input_file = input_file self.jobs = jobs - self.optimize = optimize + self.optimize = optimize_ self.jpeg_quality = jpeg_quality self.png_quality = png_quality self.jbig2_page_group_size = 0 self.jbig2_lossy = jb2lossy + self.quiet = True + self.progress_bar = False - logging.basicConfig(level=logging.DEBUG) - log = logging.getLogger() - - ctx = JobContext() + infile = Path(infile) options = OptimizeOptions( + input_file=infile, jobs=jobs, - optimize=int(level), + optimize_=int(level), jpeg_quality=0, # Use default png_quality=0, jb2lossy=False, ) - ctx.set_options(options) with TemporaryDirectory() as td: + context = PdfContext(options, td, infile, None, None) tmpout = Path(td) / 'out.pdf' - optimize(infile, tmpout, log, ctx) + optimize( + infile, + tmpout, + context, + dict( + compress_streams=True, + preserve_pdfa=True, + object_stream_mode=pikepdf.ObjectStreamMode.generate, + ), + ) copy(fspath(tmpout), fspath(outfile)) diff --git a/src/ocrmypdf/pdfa.py b/src/ocrmypdf/pdfa.py index 684254e7..4eaee8f1 100644 --- a/src/ocrmypdf/pdfa.py +++ b/src/ocrmypdf/pdfa.py @@ -1,131 +1,118 @@ -# © 2015 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + """ -Generate a PDFMARK file for Ghostscript >= 9.14, for PDF/A conversion - -pdfmark is an extension to the Postscript language that describes some PDF -features like bookmarks and annotations. It was originally specified Adobe -Distiller, for Postscript to PDF conversion: -https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf - -Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript -to create a PDF/A, we need to create a pdfmark file with the necessary metadata. - -This takes care of the many version-specific bugs and pecularities in -Ghostscript's handling of pdfmark. - +Utilities for PDF/A production and confirmation with Ghostspcript. """ -import os -from binascii import hexlify +import base64 from pathlib import Path -from string import Template - -import pkg_resources +from typing import Dict, Iterator, Union import pikepdf +import pkg_resources ICC_PROFILE_RELPATH = 'data/sRGB.icc' SRGB_ICC_PROFILE = pkg_resources.resource_filename('ocrmypdf', ICC_PROFILE_RELPATH) -# This is a template written in PostScript which is needed to create PDF/A -# files, from the Ghostscript documentation. Lines beginning with % are -# comments. Python substitution variables have a '$' prefix. -pdfa_def_template = u"""%! -% Define entries in the document Info dictionary : -/ICCProfile $icc_profile -def +def _postscript_objdef( + alias: str, + dictionary: Dict[str, str], + *, + stream_name: str = None, + stream_data: bytes = None, +) -> Iterator[str]: + assert (stream_name is None) == (stream_data is None) -% Define an ICC profile : + objtype = '/stream' if stream_name else '/dict' -[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark -[{icc_PDFA} -<< - /N currentpagedevice /ProcessColorModel known { - currentpagedevice /ProcessColorModel get dup /DeviceGray eq - {pop 1} { - /DeviceRGB eq - {3}{4} ifelse - } ifelse - } { - (ERROR, unable to determine ProcessColorModel) == flush - } ifelse ->> /PUT pdfmark -[{icc_PDFA} ICCProfile (r) file /PUT pdfmark + if stream_name: + assert stream_data is not None + a85_data = base64.a85encode(stream_data, adobe=True).decode('ascii') + yield f'{stream_name} ' + a85_data + yield 'def' -% Define the output intent dictionary : + if alias != '{Catalog}': # Catalog needs no definition + yield f'[/_objdef {alias} /type {objtype} /OBJ pdfmark' -[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark -[{OutputIntent_PDFA} << - /Type /OutputIntent % Must be so (the standard requires). - /S /GTS_PDFA1 % Must be so (the standard requires). - /DestOutputProfile {icc_PDFA} % Must be so (see above). - /OutputConditionIdentifier ($icc_identifier) ->> /PUT pdfmark -[{Catalog} <> /PUT pdfmark -""" + yield f'[{alias} <<' + for key, val in dictionary.items(): + yield f' {key} {val}' + yield '>> /PUT pdfmark' + + if stream_name: + yield f'[{alias} {stream_name[1:]} /PUT pdfmark' -def generate_pdfa_ps(target_filename, icc='sRGB'): - """Create a Postscript pdfmark file for Ghostscript PDF/A conversion +def _make_postscript(icc_name: str, icc_data: bytes, colors: int) -> Iterator[str]: + yield '%!' + yield from _postscript_objdef( + '{icc_PDFA}', # Not an f-string + {'/N': str(colors)}, + stream_name='/ICCProfile', + stream_data=icc_data, + ) + yield '' + yield from _postscript_objdef( + '{OutputIntent_PDFA}', + { + '/Type': '/OutputIntent', + '/S': '/GTS_PDFA1', + '/DestOutputProfile': '{icc_PDFA}', + '/OutputConditionIdentifier': f'({icc_name})', # Only f-string + }, + ) + yield '' + yield from _postscript_objdef( + '{Catalog}', {'/OutputIntents': '[ {OutputIntent_PDFA} ]'} + ) - A pdfmark file is a small Postscript program that provides some information - Ghostscript needs to perform PDF/A conversion. The only information we put - in specifies that we want the file to be a PDF/A, and we want to Ghostscript - to convert objects to the sRGB colorspace if it runs into any object that - it decides must be converted. - See the Adobe pdfmark Reference for details: - https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf +def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'): + """Create a Postscript PDFMARK file for Ghostscript PDF/A conversion - :param target_filename: filename to save - :param icc: ICC identifier such as 'sRGB' + pdfmark is an extension to the Postscript language that describes some PDF + features like bookmarks and annotations. It was originally specified Adobe + Distiller, for Postscript to PDF conversion. - :returns: a string containing the entire pdfmark + Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript + to create a PDF/A, we need to create a pdfmark file with the necessary metadata. + + This function takes care of the many version-specific bugs and pecularities in + Ghostscript's handling of pdfmark. + + The only information we put in specifies that we want the file to be a + PDF/A, and we want to Ghostscript to convert objects to the sRGB colorspace + if it runs into any object that it decides must be converted. + + Arguments: + target_filename: filename to save + icc: ICC identifier such as 'sRGB' + References: + Adobe PDFMARK Reference: https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf """ if icc == 'sRGB': icc_profile = SRGB_ICC_PROFILE else: raise NotImplementedError("Only supporting sRGB") - # pdfmark must contain the full path to the ICC profile, and pdfmark must be - # also encoded in ASCII. ocrmypdf can be installed anywhere, including to - # paths that have a non-ASCII character in the filename. Ghostscript - # accepts hex-encoded strings and converts them to byte strings, so - # we encode the path with fsencode() and use the hex representation. - # UTF-16 not accepted here. (Even though ASCII encodable is the usual case, - # do this always to avoid making it a rare conditional.) - bytes_icc_profile = os.fsencode(icc_profile) - hex_icc_profile = hexlify(bytes_icc_profile) - icc_profile = '<' + hex_icc_profile.decode('ascii') + '>' - - t = Template(pdfa_def_template) - ps = t.substitute(icc_profile=icc_profile, icc_identifier=icc) + bytes_icc_profile = Path(icc_profile).read_bytes() + ps = '\n'.join(_make_postscript(icc, bytes_icc_profile, 3)) # We should have encoded everything to pure ASCII by this point, and # to be safe, only allow ASCII in PostScript Path(target_filename).write_text(ps, encoding='ascii') + return target_filename -def file_claims_pdfa(filename): - """Determines if the file claims to be PDF/A compliant +def file_claims_pdfa(filename: Path): + """Determines if the file claims to be PDF/A compliant. This only checks if the XMP metadata contains a PDF/A marker. It does not do full PDF/A validation. @@ -141,7 +128,7 @@ def file_claims_pdfa(filename): } valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'} conformance = f'PDF/A-{pdfmeta.pdfa_status}' - pdfa_dict = {} + pdfa_dict: Dict[str, Union[str, bool]] = {} if pdfmeta.pdfa_status in valid_part_conforms: pdfa_dict['pass'] = True pdfa_dict['output'] = 'pdfa' diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index aaad8ebe..2c9a1be9 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -1,817 +1,9 @@ #!/usr/bin/env python3 # © 2015 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. -from collections import namedtuple -from decimal import Decimal -from enum import Enum -from math import hypot, isclose -from os import fspath -from pathlib import Path -from unittest.mock import Mock -from warnings import warn -import re -from pikepdf import PdfMatrix -import pikepdf - -from . import ghosttext - -from ..exceptions import EncryptedPdfError, MissingDependencyError - - -Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000') - -Encoding = Enum( - 'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + 'runlength' -) - -FRIENDLY_COLORSPACE = { - '/DeviceGray': Colorspace.gray, - '/CalGray': Colorspace.gray, - '/DeviceRGB': Colorspace.rgb, - '/CalRGB': Colorspace.rgb, - '/DeviceCMYK': Colorspace.cmyk, - '/Lab': Colorspace.lab, - '/ICCBased': Colorspace.icc, - '/Indexed': Colorspace.index, - '/Separation': Colorspace.sep, - '/DeviceN': Colorspace.devn, - '/Pattern': Colorspace.pattern, - '/G': Colorspace.gray, # Abbreviations permitted in inline images - '/RGB': Colorspace.rgb, - '/CMYK': Colorspace.cmyk, - '/I': Colorspace.index, -} - -FRIENDLY_ENCODING = { - '/CCITTFaxDecode': Encoding.ccitt, - '/DCTDecode': Encoding.jpeg, - '/JPXDecode': Encoding.jpeg2000, - '/JBIG2Decode': Encoding.jbig2, - '/CCF': Encoding.ccitt, # Abbreviations permitted in inline images - '/DCT': Encoding.jpeg, - '/AHx': Encoding.asciihex, - '/A85': Encoding.ascii85, - '/LZW': Encoding.lzw, - '/Fl': Encoding.flate, - '/RL': Encoding.runlength, -} - -FRIENDLY_COMP = { - Colorspace.gray: 1, - Colorspace.rgb: 3, - Colorspace.cmyk: 4, - Colorspace.lab: 3, - Colorspace.index: 1, -} - - -UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0) - - -def _is_unit_square(shorthand): - values = map(float, shorthand) - pairwise = zip(values, UNIT_SQUARE) - return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise]) - - -XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_depth']) - -InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth']) - -ContentsInfo = namedtuple( - 'ContentsInfo', ['xobject_settings', 'inline_images', 'found_vector'] -) - -TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt']) - - -class VectorInfo: - def __init__(self): - pass - - -def _normalize_stack(graphobjs): - """Convert runs of qQ's in the stack into single graphobjs""" - for operands, operator in graphobjs: - operator = str(operator) - if re.match(r'Q*q+$', operator): # Zero or more Q, one or more q - for char in operator: # Split into individual - yield ([], char) # Yield individual - else: - yield (operands, operator) - - -def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE): - """Interpret the PDF content stream. - - The stack represents the state of the PDF graphics stack. We are only - interested in the current transformation matrix (CTM) so we only track - this object; a full implementation would need to track many other items. - - The CTM is initialized to the mapping from user space to device space. - PDF units are 1/72". In a PDF viewer or printer this matrix is initialized - to the transformation to device space. For example if set to - (1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches. - - Images are always considered to be (0, 0) -> (1, 1). Before drawing an - image there should be a 'cm' that sets up an image coordinate system - where drawing from (0, 0) -> (1, 1) will draw on the desired area of the - page. - - PDF units suit our needs so we initialize ctm to the identity matrix. - - According to the PDF specification, the maximum stack depth is 32. Other - viewers tolerate some amount beyond this. We issue a warning if the - stack depth exceeds the spec limit and set a hard limit beyond this to - bound our memory requirements. If the stack underflows behavior is - undefined in the spec, but we just pretend nothing happened and leave the - CTM unchanged. - """ - - stack = [] - ctm = PdfMatrix(initial_shorthand) - xobject_settings = [] - inline_images = [] - found_vector = False - vector_ops = set('S s f F f* B B* b b*'.split()) - image_ops = set('BI ID EI q Q Do cm'.split()) - operator_whitelist = ' '.join(vector_ops | image_ops) - - for n, graphobj in enumerate( - _normalize_stack( - pikepdf.parse_content_stream(contentstream, operator_whitelist) - ) - ): - operands, operator = graphobj - if operator == 'q': - stack.append(ctm) - if len(stack) > 32: # See docstring - if len(stack) > 128: - raise RuntimeError( - "PDF graphics stack overflowed hard limit, operator %i" % n - ) - warn("PDF graphics stack overflowed spec limit") - elif operator == 'Q': - try: - ctm = stack.pop() - except IndexError: - # Keeping the ctm the same seems to be the only sensible thing - # to do. Just pretend nothing happened, keep calm and carry on. - warn("PDF graphics stack underflowed - PDF may be malformed") - elif operator == 'cm': - ctm = PdfMatrix(operands) @ ctm - elif operator == 'Do': - image_name = operands[0] - settings = XobjectSettings( - name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack) - ) - xobject_settings.append(settings) - elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this - iimage = operands[0] - inline = InlineSettings( - iimage=iimage, shorthand=ctm.shorthand, stack_depth=len(stack) - ) - inline_images.append(inline) - elif operator in vector_ops: - found_vector = True - - return ContentsInfo( - xobject_settings=xobject_settings, - inline_images=inline_images, - found_vector=found_vector, - ) - - -def _get_dpi(ctm_shorthand, image_size): - """Given the transformation matrix and image size, find the image DPI. - - PDFs do not include image resolution information within image data. - Instead, the PDF page content stream describes the location where the - image will be rasterized, and the effective resolution is the ratio of the - pixel size to raster target size. - - Normally a scanned PDF has the paper size set appropriately but this is - not guaranteed. The most common case is a cropped image will change the - page size (/CropBox) without altering the page content stream. That means - it is not sufficient to assume that the image fills the page, even though - that is the most common case. - - A PDF image may be scaled (always), cropped, translated, rotated in place - to an arbitrary angle (rarely) and skewed. Only equal area mappings can - be expressed, that is, it is not necessary to consider distortions where - the effective DPI varies with position. - - To determine the image scale, transform an offset axis vector v0 (0, 0), - width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix, - which gives the dimensions of the image in PDF units. From there we can - compare to actual image dimensions. PDF uses - row vector * matrix_tranposed unlike the traditional - matrix * column vector. - - The offset, width and height vectors can be combined in a matrix and - multiplied by the transform matrix. Then we want to calculated - magnitude(width_vector - offset_vector) - and - magnitude(height_vector - offset_vector) - - When the above is worked out algebraically, the effect of translation - cancels out, and the vector magnitudes become functions of the nonzero - transformation matrix indices. The results of the derivation are used - in this code. - - pdfimages -list does calculate the DPI in some way that is not completely - naive, but it does not get the DPI of rotated images right, so cannot be - used anymore to validate this. Photoshop works, or using Acrobat to - rotate the image back to normal. - - It does not matter if the image is partially cropped, or even out of the - /MediaBox. - - """ - - a, b, c, d, _, _ = ctm_shorthand - - # Calculate the width and height of the image in PDF units - image_drawn_width = hypot(a, b) - image_drawn_height = hypot(c, d) - - # The scale of the image is pixels per unit of default user space (1/72") - scale_w = image_size[0] / image_drawn_width - scale_h = image_size[1] / image_drawn_height - - # DPI = scale * 72 - dpi_w = scale_w * 72.0 - dpi_h = scale_h * 72.0 - - return dpi_w, dpi_h - - -class ImageInfo: - DPI_PREC = Decimal('1.000') - - def __init__(self, *, name='', pdfimage=None, inline=None, shorthand=None): - - self._name = str(name) - self._shorthand = shorthand - - if inline is not None: - self._origin = 'inline' - pim = inline.iimage - elif pdfimage is not None: - self._origin = 'xobject' - pim = pikepdf.PdfImage(pdfimage) - self._width = pim.width - self._height = pim.height - - # If /ImageMask is true, then this image is a stencil mask - # (Images that draw with this stencil mask will have a reference to - # it in their /Mask, but we don't actually need that information) - if pim.image_mask: - self._type = 'stencil' - else: - self._type = 'image' - - self._bpc = int(pim.bits_per_component) - try: - self._enc = FRIENDLY_ENCODING.get(pim.filters[0], 'image') - except IndexError: - self._enc = '?' - - try: - self._color = FRIENDLY_COLORSPACE.get(pim.colorspace, '?') - except NotImplementedError: - self._color = '?' - if self._enc == Encoding.jpeg2000: - self._color = Colorspace.jpeg2000 - - self._comp = FRIENDLY_COMP.get(self._color, '?') - - # Bit of a hack... infer grayscale if component count is uncertain - # but encoding must be monochrome. This happens if a monochrome image - # has an ICC profile attached. Better solution would be to examine - # the ICC profile. - if self._comp == '?' and self._enc in (Encoding.ccitt, 'jbig2'): - self._comp = FRIENDLY_COMP[Colorspace.gray] - - @property - def name(self): - return self._name - - @property - def type_(self): - return self._type - - @property - def width(self): - return self._width - - @property - def height(self): - return self._height - - @property - def bpc(self): - return self._bpc - - @property - def color(self): - return self._color - - @property - def comp(self): - return self._comp - - @property - def enc(self): - return self._enc - - @property - def xres(self): - return _get_dpi(self._shorthand, (self._width, self._height))[0] - - @property - def yres(self): - return _get_dpi(self._shorthand, (self._width, self._height))[1] - - def __repr__(self): - class_locals = { - attr: getattr(self, attr, None) - for attr in dir(self) - if not attr.startswith('_') - } - return ( - "" - ).format(**class_locals) - - -def _find_inline_images(contentsinfo): - "Find inline images in the contentstream" - - for n, inline in enumerate(contentsinfo.inline_images): - yield ImageInfo( - name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline - ) - - -def _image_xobjects(container): - """Search for all XObject-based images in the container - - Usually the container is a page, but it could also be a Form XObject - that contains images. Filter out the Form XObjects which are dealt with - elsewhere. - - Generate a sequence of tuples (image, xobj container), where container, - where xobj is the name of the object and image is the object itself, - since the object does not know its own name. - - """ - - if '/Resources' not in container: - return - resources = container['/Resources'] - if '/XObject' not in resources: - return - xobjs = resources['/XObject'].as_dict() - for xobj in xobjs: - candidate = xobjs[xobj] - if not '/Subtype' in candidate: - continue - if candidate['/Subtype'] == '/Image': - pdfimage = candidate - yield (pdfimage, xobj) - - -def _find_regular_images(container, contentsinfo): - """Find images stored in the container's /Resources /XObject - - Usually the container is a page, but it could also be a Form XObject - that contains images. - - Generates images with their DPI at time of drawing. - """ - - for pdfimage, xobj in _image_xobjects(container): - - # For each image that is drawn on this, check if we drawing the - # current image - yes this is O(n^2), but n == 1 almost always - for draw in contentsinfo.xobject_settings: - if draw.name != xobj: - continue - - if draw.stack_depth == 0 and _is_unit_square(draw.shorthand): - # At least one PDF in the wild (and test suite) draws an image - # when the graphics stack depth is 0, meaning that the image - # gets drawn into a square of 1x1 PDF units (or 1/72", - # or 0.35 mm). The equivalent DPI will be >100,000. Exclude - # these from our DPI calculation for the page. - continue - - yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand) - - -def _find_form_xobject_images(pdf, container, contentsinfo): - """Find any images that are in Form XObjects in the container - - The container may be a page, or a parent Form XObject. - - """ - if '/Resources' not in container: - return - resources = container['/Resources'] - if '/XObject' not in resources: - return - xobjs = resources['/XObject'].as_dict() - for xobj in xobjs: - candidate = xobjs[xobj] - if candidate['/Subtype'] != '/Form': - continue - - form_xobject = candidate - for settings in contentsinfo.xobject_settings: - if settings.name != xobj: - continue - - # Find images once for each time this Form XObject is drawn. - # This could be optimized to cache the multiple drawing events - # but in practice both Form XObjects and multiple drawing of the - # same object are both very rare. - ctm_shorthand = settings.shorthand - yield from _process_content_streams( - pdf=pdf, container=form_xobject, shorthand=ctm_shorthand - ) - - -def _process_content_streams(*, pdf, container, shorthand=None): - """Find all individual instances of images drawn in the container - - Usually the container is a page, but it may also be a Form XObject. - - On a typical page images are stored inline or as regular images - in an XObject. - - Form XObjects may include inline images, XObject images, - and recursively, other Form XObjects; and also vector graphic objects. - - Every instance of an image being drawn somewhere is flattened and - treated as a unique image, since if the same image is drawn multiple times - on one page it may be drawn at differing resolutions, and our objective - is to find the resolution at which the page can be rastered without - downsampling. - - """ - - if container.get('/Type') == '/Page' and '/Contents' in container: - initial_shorthand = shorthand or UNIT_SQUARE - elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form': - # Set the CTM to the state it was when the "Do" operator was - # encountered that is drawing this instance of the Form XObject - ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity() - - # A Form XObject may provide its own matrix to map form space into - # user space. Get this if one exists - form_shorthand = container.get('/Matrix', PdfMatrix.identity()) - form_matrix = PdfMatrix(form_shorthand) - - # Concatenate form matrix with CTM to ensure CTM is correct for - # drawing this instance of the XObject - ctm = form_matrix @ ctm - initial_shorthand = ctm.shorthand - else: - return - - contentsinfo = _interpret_contents(container, initial_shorthand) - - if contentsinfo.found_vector: - yield VectorInfo() - yield from _find_inline_images(contentsinfo) - yield from _find_regular_images(container, contentsinfo) - yield from _find_form_xobject_images(pdf, container, contentsinfo) - - -def _page_has_text(text_blocks, page_width, page_height): - """Smarter text detection that ignores text in margins""" - - pw, ph = float(page_width), float(page_height) - - margin_ratio = 0.125 - interior_bbox = ( - margin_ratio * pw, # left - (1 - margin_ratio) * ph, # top - (1 - margin_ratio) * pw, # right - margin_ratio * ph, # bottom (first quadrant: bottom < top) - ) - - def rects_intersect(a, b): - """ - Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3) - https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other - Formula assumes all boxes are in first quadrant - """ - return a[0] < b[2] and a[2] > b[0] and a[1] > b[3] and a[3] < b[1] - - has_text = False - for bbox in text_blocks: - if rects_intersect(bbox, interior_bbox): - has_text = True - break - return has_text - - -def simplify_textboxes(miner, textbox_getter): - """Extract only limited content from text boxes - - We do this to save memory and ensure that our objects are pickleable. - """ - for box in textbox_getter(miner): - first_line = box._objs[0] - first_char = first_line._objs[0] - - visible = first_char.rendermode != 3 - corrupt = first_char.get_text() == '\ufffd' - yield TextboxInfo(box.bbox, visible, corrupt) - - -def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext): - pageinfo = {} - pageinfo['pageno'] = pageno - pageinfo['images'] = [] - - page = pdf.pages[pageno] - mediabox = [Decimal(d) for d in page.MediaBox.as_list()] - width_pt = mediabox[2] - mediabox[0] - height_pt = mediabox[3] - mediabox[1] - - if xmltext is not None: - bboxes = ghosttext.page_get_textblocks( - fspath(infile), pageno, xmltext=xmltext, height=height_pt - ) - pageinfo['bboxes'] = bboxes - else: - # pdfminer required for this section - try: - from .layout import get_page_analysis, get_text_boxes - except ImportError: - raise MissingDependencyError( - "pdfminer is required for this feature. Your distribution " - "may not have installed it." - ) - pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5') - miner = get_page_analysis(infile, pageno, pscript5_mode) - pageinfo['textboxes'] = list(simplify_textboxes(miner, get_text_boxes)) - bboxes = (box.bbox for box in pageinfo['textboxes']) - - pageinfo['has_text'] = _page_has_text(bboxes, width_pt, height_pt) - - userunit = page.get('/UserUnit', Decimal(1.0)) - if not isinstance(userunit, Decimal): - userunit = Decimal(userunit) - pageinfo['userunit'] = userunit - pageinfo['width_inches'] = width_pt * userunit / Decimal(72.0) - pageinfo['height_inches'] = height_pt * userunit / Decimal(72.0) - - try: - pageinfo['rotate'] = int(page['/Rotate']) - except KeyError: - pageinfo['rotate'] = 0 - - userunit_shorthand = (userunit, 0, 0, userunit, 0, 0) - contentsinfo = [ - ci - for ci in _process_content_streams( - pdf=pdf, container=page, shorthand=userunit_shorthand - ) - ] - - pageinfo['has_vector'] = False - if any(isinstance(ci, VectorInfo) for ci in contentsinfo): - pageinfo['has_vector'] = True - - pageinfo['images'] = [im for im in contentsinfo if isinstance(im, ImageInfo)] - if pageinfo['images']: - xres = Decimal(max(image.xres for image in pageinfo['images'])) - yres = Decimal(max(image.yres for image in pageinfo['images'])) - pageinfo['xres'], pageinfo['yres'] = xres, yres - pageinfo['width_pixels'] = int(round(xres * pageinfo['width_inches'])) - pageinfo['height_pixels'] = int(round(yres * pageinfo['height_inches'])) - - return pageinfo - - -def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None): - if not log: - log = Mock() - - pdf = pikepdf.open(infile) # Do not close in this function - if pdf.is_encrypted: - pdf.close() - raise EncryptedPdfError() # Triggered by encryption with empty passwd - if detailed_analysis: - pages_xml = None - else: - pages_xml = ghosttext.extract_text_xml(infile, pdf, pageno=None, log=log) - - pages = [] - for n in range(len(pdf.pages)): - page_xml = pages_xml[n] if pages_xml else None - page = PageInfo(pdf, n, infile, page_xml, detailed_analysis) - pages.append(page) - - return pages, pdf - - -class PageInfo: - def __init__(self, pdf, pageno, infile, xmltext, detailed_analysis=False): - self._pageno = pageno - self._infile = infile - self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile, xmltext) - self._detailed_analysis = detailed_analysis - - @property - def pageno(self): - return self._pageno - - @property - def has_text(self): - return self._pageinfo['has_text'] - - @property - def has_corrupt_text(self): - if not self._detailed_analysis: - raise NotImplementedError('Did not do detailed analysis') - return any(tbox.is_corrupt for tbox in self._pageinfo['textboxes']) - - @property - def has_vector(self): - return self._pageinfo['has_vector'] - - @property - def width_inches(self): - return self._pageinfo['width_inches'] - - @property - def height_inches(self): - return self._pageinfo['height_inches'] - - @property - def width_pixels(self): - return int(round(self.width_inches * self.xres)) - - @property - def height_pixels(self): - return int(round(self.height_inches * self.yres)) - - @property - def rotation(self): - return self._pageinfo.get('rotate', None) - - @rotation.setter - def rotation(self, value): - if value in (0, 90, 180, 270, 360, -90, -180, -270): - self._pageinfo['rotate'] = value - else: - raise ValueError("rotation must be a cardinal angle") - - @property - def images(self): - return self._pageinfo['images'] - - def get_textareas(self, visible=None, corrupt=None): - def predicate(obj, want_visible, want_corrupt): - result = True - if want_visible is not None: - if obj.is_visible != want_visible: - result = False - if want_corrupt is not None: - if obj.is_corrupt != want_corrupt: - result = False - return result - - if 'textboxes' not in self._pageinfo: - if visible is not None and corrupt is not None: - raise NotImplementedError('Ghostscript textboxes cannot be classified') - return self._pageinfo['bboxes'] - - return ( - obj.bbox - for obj in self._pageinfo['textboxes'] - if predicate(obj, visible, corrupt) - ) - - @property - def xres(self): - return self._pageinfo.get('xres', None) - - @property - def yres(self): - return self._pageinfo.get('yres', None) - - @property - def userunit(self): - return self._pageinfo.get('userunit', None) - - @property - def min_version(self): - if self.userunit is not None: - return '1.6' - else: - return '1.5' - - def __repr__(self): - return ( - '' - ).format( - self.pageno, - self.width_inches, - self.height_inches, - self.rotation, - self.xres, - self.yres, - self.has_text, - ) - - -class PdfInfo: - """Get summary information about a PDF""" - - def __init__(self, infile, detailed_page_analysis=False, log=None): - self._infile = infile - self._pages, pdf = _pdf_get_all_pageinfo( - infile, detailed_page_analysis, log=log - ) - self._needs_rendering = pdf.root.get('/NeedsRendering', False) - self._has_acroform = False - if '/AcroForm' in pdf.root: - if len(pdf.root.AcroForm.get('/Fields', [])) > 0: - self._has_acroform = True - elif '/XFA' in pdf.root.AcroForm: - self._has_acroform = True - pdf.close() - - @property - def pages(self): - return self._pages - - @property - def min_version(self): - # The minimum PDF is the maximum version that any particular page needs - return max(page.min_version for page in self.pages) - - @property - def has_userunit(self): - return any(page.userunit != 1.0 for page in self.pages) - - @property - def has_acroform(self): - return self._has_acroform - - @property - def filename(self): - if not isinstance(self._infile, (str, Path)): - raise NotImplementedError("can't get filename from stream") - return self._infile - - @property - def needs_rendering(self): - return self._needs_rendering - - def __getitem__(self, item): - return self._pages[item] - - def __len__(self): - return len(self._pages) - - def __repr__(self): - return f"" - - -def main(): - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument('infile') - args = parser.parse_args() - info = _pdf_get_all_pageinfo(args.infile) - from pprint import pprint - - pprint(info) - - -if __name__ == '__main__': - main() +from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PdfInfo diff --git a/src/ocrmypdf/pdfinfo/ghosttext.py b/src/ocrmypdf/pdfinfo/ghosttext.py deleted file mode 100644 index c1a612a5..00000000 --- a/src/ocrmypdf/pdfinfo/ghosttext.py +++ /dev/null @@ -1,98 +0,0 @@ -# © 2018 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import re -import xml.etree.ElementTree as ET - -from ..exec import ghostscript - -# Forgive me for I have sinned -# I am using regular expressions to parse XML. However the XML in this case, -# generated by Ghostscript, is self-consistent enough to be parseable. -regex_remove_char_tags = re.compile( - br""" - ] # anything single character but > - | \">\" # special case: trap ">" - )* - /> # terminate with '/>' -""", - re.VERBOSE, -) - - -def page_get_textblocks(infile, pageno, xmltext, height): - """Get text boxes out of Ghostscript txtwrite xml""" - - root = xmltext - if not hasattr(xmltext, 'findall'): - return [] - - def blocks(): - for span in root.findall('.//span'): - bbox_str = span.attrib['bbox'] - font_size = span.attrib['size'] - pts = [int(pt) for pt in bbox_str.split()] - pts[1] = pts[1] - int(float(font_size) + 0.5) - bbox_topdown = tuple(pts) - bb = bbox_topdown - bbox_bottomup = (bb[0], height - bb[3], bb[2], height - bb[1]) - yield bbox_bottomup - - def joined_blocks(): - prev = None - for bbox in blocks(): - if prev is None: - prev = bbox - if bbox[1] == prev[1] and bbox[3] == prev[3]: - gap = prev[2] - bbox[0] - height = abs(bbox[3] - bbox[1]) - if gap < height: - # Join boxes - prev = (prev[0], prev[1], bbox[2], bbox[3]) - continue - # yield previously joined bboxes and start anew - yield prev - prev = bbox - if prev is not None: - yield prev - - return [block for block in joined_blocks()] - - -def extract_text_xml(infile, pdf, pageno=None, log=None): - existing_text = ghostscript.extract_text(infile, pageno=None) - existing_text = regex_remove_char_tags.sub(b' ', existing_text) - - try: - root = ET.fromstringlist([b'\n', existing_text, b'\n']) - page_xml = root.findall('page') - except ET.ParseError as e: - log.error( - "An error occurred while attempting to retrieve existing text in " - "the input file. Will attempt to continue assuming that there is " - "no existing text in the file. The error was:" - ) - log.error(e) - page_xml = [None] * len(pdf.pages) - - page_count_difference = len(pdf.pages) - len(page_xml) - if page_count_difference != 0: - log.error("The number of pages in the input file is inconsistent.") - if page_count_difference > 0: - page_xml.extend([None] * page_count_difference) - return page_xml diff --git a/src/ocrmypdf/pdfinfo/info.py b/src/ocrmypdf/pdfinfo/info.py new file mode 100644 index 00000000..64b59b9c --- /dev/null +++ b/src/ocrmypdf/pdfinfo/info.py @@ -0,0 +1,931 @@ +#!/usr/bin/env python3 +# © 2015 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import atexit +import logging +import re +from collections import defaultdict, namedtuple +from contextlib import ExitStack +from decimal import Decimal +from enum import Enum +from functools import partial +from math import hypot, inf, isclose +from os import PathLike +from pathlib import Path +from typing import Container, Iterator, Optional, Tuple, Union +from warnings import warn + +import pikepdf +from pikepdf import Object, Pdf, PdfMatrix + +from ocrmypdf._concurrent import Executor, SerialExecutor +from ocrmypdf.exceptions import EncryptedPdfError, InputFileError +from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap +from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes + +logger = logging.getLogger() + +Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000') + +Encoding = Enum( + 'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate runlength' +) + +FRIENDLY_COLORSPACE = { + '/DeviceGray': Colorspace.gray, + '/CalGray': Colorspace.gray, + '/DeviceRGB': Colorspace.rgb, + '/CalRGB': Colorspace.rgb, + '/DeviceCMYK': Colorspace.cmyk, + '/Lab': Colorspace.lab, + '/ICCBased': Colorspace.icc, + '/Indexed': Colorspace.index, + '/Separation': Colorspace.sep, + '/DeviceN': Colorspace.devn, + '/Pattern': Colorspace.pattern, + '/G': Colorspace.gray, # Abbreviations permitted in inline images + '/RGB': Colorspace.rgb, + '/CMYK': Colorspace.cmyk, + '/I': Colorspace.index, +} + +FRIENDLY_ENCODING = { + '/CCITTFaxDecode': Encoding.ccitt, + '/DCTDecode': Encoding.jpeg, + '/JPXDecode': Encoding.jpeg2000, + '/JBIG2Decode': Encoding.jbig2, + '/CCF': Encoding.ccitt, # Abbreviations permitted in inline images + '/DCT': Encoding.jpeg, + '/AHx': Encoding.asciihex, + '/A85': Encoding.ascii85, + '/LZW': Encoding.lzw, + '/Fl': Encoding.flate, + '/RL': Encoding.runlength, +} + +FRIENDLY_COMP = { + Colorspace.gray: 1, + Colorspace.rgb: 3, + Colorspace.cmyk: 4, + Colorspace.lab: 3, + Colorspace.index: 1, +} + + +UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0) + + +def _is_unit_square(shorthand): + values = map(float, shorthand) + pairwise = zip(values, UNIT_SQUARE) + return all(isclose(a, b, rel_tol=1e-3) for a, b in pairwise) + + +XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_depth']) + +InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth']) + +ContentsInfo = namedtuple( + 'ContentsInfo', + ['xobject_settings', 'inline_images', 'found_vector', 'found_text', 'name_index'], +) + +TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt']) + + +class VectorMarker: + pass + + +class TextMarker: + pass + + +def _normalize_stack(graphobjs): + """Convert runs of qQ's in the stack into single graphobjs""" + for operands, operator in graphobjs: + operator = str(operator) + if re.match(r'Q*q+$', operator): # Zero or more Q, one or more q + for char in operator: # Split into individual + yield ([], char) # Yield individual + else: + yield (operands, operator) + + +def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE): + """Interpret the PDF content stream. + + The stack represents the state of the PDF graphics stack. We are only + interested in the current transformation matrix (CTM) so we only track + this object; a full implementation would need to track many other items. + + The CTM is initialized to the mapping from user space to device space. + PDF units are 1/72". In a PDF viewer or printer this matrix is initialized + to the transformation to device space. For example if set to + (1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches. + + Images are always considered to be (0, 0) -> (1, 1). Before drawing an + image there should be a 'cm' that sets up an image coordinate system + where drawing from (0, 0) -> (1, 1) will draw on the desired area of the + page. + + PDF units suit our needs so we initialize ctm to the identity matrix. + + According to the PDF specification, the maximum stack depth is 32. Other + viewers tolerate some amount beyond this. We issue a warning if the + stack depth exceeds the spec limit and set a hard limit beyond this to + bound our memory requirements. If the stack underflows behavior is + undefined in the spec, but we just pretend nothing happened and leave the + CTM unchanged. + """ + + stack = [] + ctm = PdfMatrix(initial_shorthand) + xobject_settings = [] + inline_images = [] + name_index = defaultdict(lambda: []) + found_vector = False + found_text = False + vector_ops = set('S s f F f* B B* b b*'.split()) + text_showing_ops = set("""TJ Tj " '""".split()) + image_ops = set('BI ID EI q Q Do cm'.split()) + operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops) + + for n, graphobj in enumerate( + _normalize_stack( + pikepdf.parse_content_stream(contentstream, operator_whitelist) + ) + ): + operands, operator = graphobj + if operator == 'q': + stack.append(ctm) + if len(stack) > 32: # See docstring + if len(stack) > 128: + raise RuntimeError( + "PDF graphics stack overflowed hard limit, operator %i" % n + ) + warn("PDF graphics stack overflowed spec limit") + elif operator == 'Q': + try: + ctm = stack.pop() + except IndexError: + # Keeping the ctm the same seems to be the only sensible thing + # to do. Just pretend nothing happened, keep calm and carry on. + warn("PDF graphics stack underflowed - PDF may be malformed") + elif operator == 'cm': + ctm = PdfMatrix(operands) @ ctm + elif operator == 'Do': + image_name = operands[0] + settings = XobjectSettings( + name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack) + ) + xobject_settings.append(settings) + name_index[image_name].append(settings) + elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this + iimage = operands[0] + inline = InlineSettings( + iimage=iimage, shorthand=ctm.shorthand, stack_depth=len(stack) + ) + inline_images.append(inline) + elif operator in vector_ops: + found_vector = True + elif operator in text_showing_ops: + found_text = True + + return ContentsInfo( + xobject_settings=xobject_settings, + inline_images=inline_images, + found_vector=found_vector, + found_text=found_text, + name_index=name_index, + ) + + +def _get_dpi(ctm_shorthand, image_size) -> Resolution: + """Given the transformation matrix and image size, find the image DPI. + + PDFs do not include image resolution information within image data. + Instead, the PDF page content stream describes the location where the + image will be rasterized, and the effective resolution is the ratio of the + pixel size to raster target size. + + Normally a scanned PDF has the paper size set appropriately but this is + not guaranteed. The most common case is a cropped image will change the + page size (/CropBox) without altering the page content stream. That means + it is not sufficient to assume that the image fills the page, even though + that is the most common case. + + A PDF image may be scaled (always), cropped, translated, rotated in place + to an arbitrary angle (rarely) and skewed. Only equal area mappings can + be expressed, that is, it is not necessary to consider distortions where + the effective DPI varies with position. + + To determine the image scale, transform an offset axis vector v0 (0, 0), + width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix, + which gives the dimensions of the image in PDF units. From there we can + compare to actual image dimensions. PDF uses + row vector * matrix_transposed unlike the traditional + matrix * column vector. + + The offset, width and height vectors can be combined in a matrix and + multiplied by the transform matrix. Then we want to calculated + magnitude(width_vector - offset_vector) + and + magnitude(height_vector - offset_vector) + + When the above is worked out algebraically, the effect of translation + cancels out, and the vector magnitudes become functions of the nonzero + transformation matrix indices. The results of the derivation are used + in this code. + + pdfimages -list does calculate the DPI in some way that is not completely + naive, but it does not get the DPI of rotated images right, so cannot be + used anymore to validate this. Photoshop works, or using Acrobat to + rotate the image back to normal. + + It does not matter if the image is partially cropped, or even out of the + /MediaBox. + + """ + + a, b, c, d, _, _ = ctm_shorthand + + # Calculate the width and height of the image in PDF units + image_drawn = hypot(a, b), hypot(c, d) + + def calc(drawn, pixels, inches_per_pt=72.0): + # The scale of the image is pixels per unit of default user space (1/72") + scale = pixels / drawn if drawn != 0 else inf + dpi = scale * inches_per_pt + return dpi + + dpi_w, dpi_h = (calc(image_drawn[n], image_size[n]) for n in range(2)) + return Resolution(dpi_w, dpi_h) + + +class ImageInfo: + DPI_PREC = Decimal('1.000') + + def __init__( + self, + *, + name='', + pdfimage: Optional[Object] = None, + inline: Optional[Object] = None, + shorthand=None, + ): + self._name = str(name) + self._shorthand = shorthand + + if inline is not None: + self._origin = 'inline' + pim = inline.iimage + elif pdfimage is not None: + self._origin = 'xobject' + pim = pikepdf.PdfImage(pdfimage) + else: + raise ValueError("Either pdfimage or inline must be set") + self._width = pim.width + self._height = pim.height + + # If /ImageMask is true, then this image is a stencil mask + # (Images that draw with this stencil mask will have a reference to + # it in their /Mask, but we don't actually need that information) + if pim.image_mask: + self._type = 'stencil' + else: + self._type = 'image' + + self._bpc = int(pim.bits_per_component) + try: + self._enc = FRIENDLY_ENCODING.get(pim.filters[0], 'image') + except IndexError: + self._enc = '?' + + try: + self._color = FRIENDLY_COLORSPACE.get(pim.colorspace, '?') + except NotImplementedError: + self._color = '?' + if self._enc == Encoding.jpeg2000: + self._color = Colorspace.jpeg2000 + + if self._color == Colorspace.icc: + # Check the ICC profile to determine actual colorspace + pim_icc = pim.icc + if pim_icc.profile.xcolor_space == 'GRAY': + self._comp = 1 + elif pim_icc.profile.xcolor_space == 'CMYK': + self._comp = 4 + else: + self._comp = 3 + else: + self._comp = FRIENDLY_COMP.get(self._color, '?') + + # Bit of a hack... infer grayscale if component count is uncertain + # but encoding only supports monochrome. + if self._comp == '?' and self._enc in (Encoding.ccitt, Encoding.jbig2): + self._comp = FRIENDLY_COMP[Colorspace.gray] + + @property + def name(self): + return self._name + + @property + def type_(self): + return self._type + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def bpc(self): + return self._bpc + + @property + def color(self): + return self._color + + @property + def comp(self): + return self._comp + + @property + def enc(self): + return self._enc + + @property + def renderable(self): + return self.dpi.is_finite and self.width >= 0 and self.height >= 0 + + @property + def dpi(self): + return _get_dpi(self._shorthand, (self._width, self._height)) + + def __repr__(self): + class_locals = { + attr: getattr(self, attr, None) + for attr in dir(self) + if not attr.startswith('_') + } + return ( + "" + ).format(**class_locals) + + +def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]: + "Find inline images in the contentstream" + + for n, inline in enumerate(contentsinfo.inline_images): + yield ImageInfo( + name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline + ) + + +def _image_xobjects(container) -> Iterator[Tuple[Object, str]]: + """Search for all XObject-based images in the container + + Usually the container is a page, but it could also be a Form XObject + that contains images. Filter out the Form XObjects which are dealt with + elsewhere. + + Generate a sequence of tuples (image, xobj container), where container, + where xobj is the name of the object and image is the object itself, + since the object does not know its own name. + + """ + + if '/Resources' not in container: + return + resources = container['/Resources'] + if '/XObject' not in resources: + return + xobjs = resources['/XObject'].as_dict() + for xobj in xobjs: + candidate: Object = xobjs[xobj] + if not '/Subtype' in candidate: + continue + if candidate['/Subtype'] == '/Image': + pdfimage = candidate + yield (pdfimage, xobj) + + +def _find_regular_images( + container: Object, contentsinfo: ContentsInfo +) -> Iterator[ImageInfo]: + """Find images stored in the container's /Resources /XObject + + Usually the container is a page, but it could also be a Form XObject + that contains images. + + Generates images with their DPI at time of drawing. + """ + + for pdfimage, xobj in _image_xobjects(container): + if xobj not in contentsinfo.name_index: + continue + for draw in contentsinfo.name_index[xobj]: + if draw.stack_depth == 0 and _is_unit_square(draw.shorthand): + # At least one PDF in the wild (and test suite) draws an image + # when the graphics stack depth is 0, meaning that the image + # gets drawn into a square of 1x1 PDF units (or 1/72", + # or 0.35 mm). The equivalent DPI will be >100,000. Exclude + # these from our DPI calculation for the page. + continue + + yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand) + + +def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: ContentsInfo): + """Find any images that are in Form XObjects in the container + + The container may be a page, or a parent Form XObject. + + """ + if '/Resources' not in container: + return + resources = container['/Resources'] + if '/XObject' not in resources: + return + xobjs = resources['/XObject'].as_dict() + for xobj in xobjs: + candidate = xobjs[xobj] + if candidate['/Subtype'] != '/Form': + continue + + form_xobject = candidate + for settings in contentsinfo.xobject_settings: + if settings.name != xobj: + continue + + # Find images once for each time this Form XObject is drawn. + # This could be optimized to cache the multiple drawing events + # but in practice both Form XObjects and multiple drawing of the + # same object are both very rare. + ctm_shorthand = settings.shorthand + yield from _process_content_streams( + pdf=pdf, container=form_xobject, shorthand=ctm_shorthand + ) + + +def _process_content_streams( + *, pdf: Pdf, container: Object, shorthand=None +) -> Iterator[Union[VectorMarker, TextMarker, ImageInfo]]: + """Find all individual instances of images drawn in the container + + Usually the container is a page, but it may also be a Form XObject. + + On a typical page images are stored inline or as regular images + in an XObject. + + Form XObjects may include inline images, XObject images, + and recursively, other Form XObjects; and also vector graphic objects. + + Every instance of an image being drawn somewhere is flattened and + treated as a unique image, since if the same image is drawn multiple times + on one page it may be drawn at differing resolutions, and our objective + is to find the resolution at which the page can be rastered without + downsampling. + + """ + + if container.get('/Type') == '/Page' and '/Contents' in container: + initial_shorthand = shorthand or UNIT_SQUARE + elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form': + # Set the CTM to the state it was when the "Do" operator was + # encountered that is drawing this instance of the Form XObject + ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity() + + # A Form XObject may provide its own matrix to map form space into + # user space. Get this if one exists + form_shorthand = container.get('/Matrix', PdfMatrix.identity()) + form_matrix = PdfMatrix(form_shorthand) + + # Concatenate form matrix with CTM to ensure CTM is correct for + # drawing this instance of the XObject + ctm = form_matrix @ ctm + initial_shorthand = ctm.shorthand + else: + return + + contentsinfo = _interpret_contents(container, initial_shorthand) + + if contentsinfo.found_vector: + yield VectorMarker() + if contentsinfo.found_text: + yield TextMarker() + yield from _find_inline_images(contentsinfo) + yield from _find_regular_images(container, contentsinfo) + yield from _find_form_xobject_images(pdf, container, contentsinfo) + + +def _page_has_text(text_blocks, page_width, page_height) -> bool: + """Smarter text detection that ignores text in margins""" + + pw, ph = float(page_width), float(page_height) + + margin_ratio = 0.125 + interior_bbox = ( + margin_ratio * pw, # left + (1 - margin_ratio) * ph, # top + (1 - margin_ratio) * pw, # right + margin_ratio * ph, # bottom (first quadrant: bottom < top) + ) + + def rects_intersect(a, b) -> bool: + """ + Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3) + https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other + Formula assumes all boxes are in first quadrant + """ + return a[0] < b[2] and a[2] > b[0] and a[1] > b[3] and a[3] < b[1] + + has_text = False + for bbox in text_blocks: + if rects_intersect(bbox, interior_bbox): + has_text = True + break + return has_text + + +def simplify_textboxes(miner, textbox_getter) -> Iterator[TextboxInfo]: + """Extract only limited content from text boxes + + We do this to save memory and ensure that our objects are pickleable. + """ + for box in textbox_getter(miner): + first_line = box._objs[0] + first_char = first_line._objs[0] + + visible = first_char.rendermode != 3 + corrupt = first_char.get_text() == '\ufffd' + yield TextboxInfo(box.bbox, visible, corrupt) + + +worker_pdf = None + + +def _pdf_pageinfo_sync_init(pdf: Pdf, infile: Path, pdfminer_loglevel): + global worker_pdf # pylint: disable=global-statement + pikepdf_enable_mmap() + + logging.getLogger('pdfminer').setLevel(pdfminer_loglevel) + + # If the pdf is not opened, open a copy for our worker process to use + if pdf is None: + worker_pdf = pikepdf.open(infile) + + def on_process_close(): + worker_pdf.close() + + # Close when this process exits + atexit.register(on_process_close) + + +def _pdf_pageinfo_sync(args): + pageno, thread_pdf, infile, check_pages, detailed_analysis = args + pdf = thread_pdf if thread_pdf is not None else worker_pdf + with ExitStack() as stack: + if not pdf: # When called with SerialExecutor + pdf = stack.enter_context(pikepdf.open(infile)) + page = PageInfo(pdf, pageno, infile, check_pages, detailed_analysis) + return page + + +def _pdf_pageinfo_concurrent( + pdf, + executor: Executor, + infile, + progbar, + max_workers, + check_pages, + detailed_analysis=False, +): + pages = [None] * len(pdf.pages) + + def update_pageinfo(result, pbar): + page = result + if not page: + raise InputFileError("Could read a page in the PDF") + pages[page.pageno] = page + pbar.update() + + if max_workers is None: + max_workers = available_cpu_count() + + total = len(pdf.pages) + + use_threads = False # No performance gain if threaded due to GIL + n_workers = min(1 + len(pages) // 4, max_workers) + if n_workers == 1: + # But if we decided on only one worker, there is no point in using + # a separate process. + use_threads = True + + # If we use a thread, we can pass the already-open Pdf for them to use + # If we use processes, we pass a None which tells the init function to open its + # own + initial_pdf = pdf if use_threads else None + + contexts = ( + (n, initial_pdf, infile, check_pages, detailed_analysis) for n in range(total) + ) + assert n_workers == 1 if use_threads else n_workers >= 1, "Not multithreadable" + executor( + use_threads=use_threads, + max_workers=n_workers, + tqdm_kwargs=dict( + total=total, desc="Scanning contents", unit='page', disable=not progbar + ), + worker_initializer=partial( + _pdf_pageinfo_sync_init, + initial_pdf, + infile, + logging.getLogger('pdfminer').level, + ), + task=_pdf_pageinfo_sync, + task_arguments=contexts, + task_finished=update_pageinfo, + ) + return pages + + +class PageInfo: + def __init__( + self, + pdf: Pdf, + pageno: int, + infile: PathLike, + check_pages: Container[int], + detailed_analysis: bool = False, + ): + self._pageno = pageno + self._infile = infile + self._detailed_analysis = detailed_analysis + self._gather_pageinfo(pdf, pageno, infile, check_pages, detailed_analysis) + + def _gather_pageinfo( + self, + pdf: Pdf, + pageno: int, + infile: PathLike, + check_pages: Container[int], + detailed_analysis: bool, + ): + page = pdf.pages[pageno] + mediabox = [Decimal(d) for d in page.MediaBox.as_list()] + width_pt = mediabox[2] - mediabox[0] + height_pt = mediabox[3] - mediabox[1] + + check_this_page = pageno in check_pages + + if check_this_page and detailed_analysis: + pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5') + miner = get_page_analysis(infile, pageno, pscript5_mode) + self._textboxes = list(simplify_textboxes(miner, get_text_boxes)) + bboxes = (box.bbox for box in self._textboxes) + + self._has_text = _page_has_text(bboxes, width_pt, height_pt) + else: + self._textboxes = [] + self._has_text = None # i.e. "no information" + + userunit = page.get('/UserUnit', Decimal(1.0)) + if not isinstance(userunit, Decimal): + userunit = Decimal(userunit) + self._userunit = userunit + self._width_inches = width_pt * userunit / Decimal(72.0) + self._height_inches = height_pt * userunit / Decimal(72.0) + + try: + self._rotate = int(page['/Rotate']) + except KeyError: + self._rotate = 0 + + userunit_shorthand = (userunit, 0, 0, userunit, 0, 0) + + if check_this_page: + self._has_vector = False + self._has_text = False + self._images = [] + for ci in _process_content_streams( + pdf=pdf, container=page, shorthand=userunit_shorthand + ): + if isinstance(ci, VectorMarker): + self._has_vector = True + elif isinstance(ci, TextMarker): + self._has_text = True + elif isinstance(ci, ImageInfo): + self._images.append(ci) + else: + raise NotImplementedError() + else: + self._has_vector = None # i.e. "no information" + self._has_text = None + self._images = None + + self._dpi = None + if self._images: + dpi = Resolution(0.0, 0.0).take_max( + image.dpi for image in self._images if image.renderable + ) + self._dpi = dpi + self._width_pixels = int(round(dpi.x * float(self._width_inches))) + self._height_pixels = int(round(dpi.y * float(self._height_inches))) + + @property + def pageno(self) -> int: + return self._pageno + + @property + def has_text(self) -> bool: + return self._has_text + + @property + def has_corrupt_text(self) -> bool: + if not self._detailed_analysis: + raise NotImplementedError('Did not do detailed analysis') + return any(tbox.is_corrupt for tbox in self._textboxes) + + @property + def has_vector(self) -> bool: + return self._has_vector + + @property + def width_inches(self) -> Decimal: + return self._width_inches + + @property + def height_inches(self) -> Decimal: + return self._height_inches + + @property + def width_pixels(self) -> int: + return int(round(float(self.width_inches) * self.dpi.x)) + + @property + def height_pixels(self) -> int: + return int(round(float(self.height_inches) * self.dpi.y)) + + @property + def rotation(self) -> int: + return self._rotate + + @rotation.setter + def rotation(self, value): + if value in (0, 90, 180, 270, 360, -90, -180, -270): + self._rotate = value + else: + raise ValueError("rotation must be a cardinal angle") + + @property + def images(self): + return self._images + + def get_textareas( + self, visible: Optional[bool] = None, corrupt: Optional[bool] = None + ): + def predicate(obj, want_visible, want_corrupt): + result = True + if want_visible is not None: + if obj.is_visible != want_visible: + result = False + if want_corrupt is not None: + if obj.is_corrupt != want_corrupt: + result = False + return result + + if not self._textboxes: + if visible is not None and corrupt is not None: + raise NotImplementedError('Incomplete information on textboxes') + return self._textboxes + + return (obj.bbox for obj in self._textboxes if predicate(obj, visible, corrupt)) + + @property + def dpi(self) -> Resolution: + if self._dpi is None: + return Resolution(0.0, 0.0) + return self._dpi + + @property + def userunit(self) -> Decimal: + return self._userunit + + @property + def min_version(self) -> str: + if self.userunit is not None: + return '1.6' + else: + return '1.5' + + def __repr__(self): + return ( + f'' + ) + + +class PdfInfo: + """Get summary information about a PDF""" + + def __init__( + self, + infile, + *, + detailed_analysis: bool = False, + progbar: bool = False, + max_workers: int = None, + check_pages=None, + executor: Executor = SerialExecutor(), + ): + self._infile = infile + if check_pages is None: + check_pages = range(0, 1_000_000_000) + + with pikepdf.open(infile) as pdf: + if pdf.is_encrypted: + raise EncryptedPdfError() # Triggered by encryption with empty passwd + self._pages = _pdf_pageinfo_concurrent( + pdf, + executor, + infile, + progbar, + max_workers, + check_pages=check_pages, + detailed_analysis=detailed_analysis, + ) + self._needs_rendering = pdf.Root.get('/NeedsRendering', False) + self._has_acroform = False + if '/AcroForm' in pdf.Root: + if len(pdf.Root.AcroForm.get('/Fields', [])) > 0: + self._has_acroform = True + elif '/XFA' in pdf.Root.AcroForm: + self._has_acroform = True + + @property + def pages(self): + return self._pages + + @property + def min_version(self) -> str: + # The minimum PDF is the maximum version that any particular page needs + return max(page.min_version for page in self.pages) + + @property + def has_userunit(self) -> bool: + return any(page.userunit != 1.0 for page in self.pages) + + @property + def has_acroform(self) -> bool: + return self._has_acroform + + @property + def filename(self) -> Union[str, Path]: + if not isinstance(self._infile, (str, Path)): + raise NotImplementedError("can't get filename from stream") + return self._infile + + @property + def needs_rendering(self) -> bool: + return self._needs_rendering + + def __getitem__(self, item) -> PageInfo: + return self._pages[item] + + def __len__(self): + return len(self._pages) + + def __repr__(self): + return f"" + + +def main(): + import argparse # pylint: disable=import-outside-toplevel + from pprint import pprint # pylint: disable=import-outside-toplevel + + parser = argparse.ArgumentParser() + parser.add_argument('infile') + args = parser.parse_args() + pdfinfo = PdfInfo(args.infile) + + pprint(pdfinfo) + for page in pdfinfo.pages: + pprint(page) + for im in page.images: + pprint(im) + + +if __name__ == '__main__': + main() diff --git a/src/ocrmypdf/pdfinfo/layout.py b/src/ocrmypdf/pdfinfo/layout.py index e6d04c9c..4159a1cb 100644 --- a/src/ocrmypdf/pdfinfo/layout.py +++ b/src/ocrmypdf/pdfinfo/layout.py @@ -1,85 +1,30 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + import re from math import copysign from pathlib import Path from unittest.mock import patch +import pdfminer import pdfminer.encodingdb import pdfminer.pdfdevice import pdfminer.pdfinterp from pdfminer.converter import PDFLayoutAnalyzer -from pdfminer.glyphlist import glyphname2unicode from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox from pdfminer.pdfdocument import PDFTextExtractionNotAllowed -from pdfminer.pdffont import PDFFont, PDFSimpleFont, PDFUnicodeNotDefined +from pdfminer.pdffont import PDFSimpleFont, PDFUnicodeNotDefined from pdfminer.pdfpage import PDFPage from pdfminer.utils import bbox2str, matrix2str -from ..exceptions import EncryptedPdfError +from ocrmypdf.exceptions import EncryptedPdfError, InputFileError STRIP_NAME = re.compile(r'[0-9]+') -# -# Unconditional pdfminer patches -# - - -def name2unicode(name): - """Fix pdfminer's name2unicode function - - Font cids that are mapped to names of the form /g123 seem to be, by convention - characters with no corresponding Unicode entry. These can be subsetted fonts - or symbolic fonts. There seems to be no way to map /g123 fonts to Unicode, - barring a ToUnicode data structure. - """ - if name in glyphname2unicode: - return glyphname2unicode[name] - if name.startswith('g') or name.startswith('a'): - raise KeyError(name) - if name.startswith('uni'): - try: - return chr(int(name[3:], 16)) - except ValueError: # Not hexadecimal - raise KeyError(name) - m = STRIP_NAME.search(name) - if not m: - raise KeyError(name) - return chr(int(m.group(0))) - - -pdfminer.encodingdb.name2unicode = name2unicode - -original_PDFFont_init = PDFFont.__init__ - - -def PDFFont__init__(self, descriptor, widths, default_width=None): - original_PDFFont_init(self, descriptor, widths, default_width) - # PDF spec says descent should be negative - # A font with a positive descent implies it floats entirely above the - # baseline, i.e. it's not really a baseline anymore. I have fonts that - # claim a positive descent, but treating descent as positive always seems - # to misposition text. - if self.descent > 0: - self.descent = -self.descent - - -PDFFont.__init__ = PDFFont__init__ original_PDFSimpleFont_init = PDFSimpleFont.__init__ @@ -97,6 +42,7 @@ def PDFSimpleFont__init__(self, descriptor, widths, spec): PDFSimpleFont.__init__ = PDFSimpleFont__init__ + # # pdfminer patches when creator is PScript5.dll # @@ -172,6 +118,7 @@ class LTStateAwareChar(LTChar): - the Unicode mapping is known, and both have the same render mode - the Unicode mapping is unknown but both are part of the same font """ + # pylint: disable=protected-access both_unicode_mapped = isinstance(self._text, str) and isinstance(obj._text, str) try: if both_unicode_mapped: @@ -184,7 +131,7 @@ class LTStateAwareChar(LTChar): def get_text(self): if isinstance(self._text, tuple): - return '�' + return '\ufffd' # standard 'Unknown symbol' return self._text def __repr__(self): @@ -206,6 +153,7 @@ class TextPositionTracker(PDFLayoutAnalyzer): super().__init__(rsrcmgr, pageno, laparams) self.textstate = None self.result = None + self.cur_item = None # not defined in pdfminer code as it should be def begin_page(self, page, ctm): super().begin_page(page, ctm) @@ -262,9 +210,20 @@ class TextPositionTracker(PDFLayoutAnalyzer): def get_page_analysis(infile, pageno, pscript5_mode): rman = pdfminer.pdfinterp.PDFResourceManager(caching=True) - dev = TextPositionTracker(rman, laparams=LAParams()) + if pdfminer.__version__ < '20200402': + # Workaround for https://github.com/pdfminer/pdfminer.six/issues/395 + disable_boxes_flow = 2 + else: + disable_boxes_flow = None + dev = TextPositionTracker( + rman, + laparams=LAParams( + all_texts=True, detect_vertical=True, boxes_flow=disable_boxes_flow + ), + ) interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev) + patcher = None if pscript5_mode: patcher = patch.multiple( 'pdfminer.pdffont.PDFType3Font', @@ -277,12 +236,17 @@ def get_page_analysis(infile, pageno, pscript5_mode): try: with Path(infile).open('rb') as f: - page = PDFPage.get_pages(f, pagenos=[pageno], maxpages=0) - interp.process_page(next(page)) - except PDFTextExtractionNotAllowed: - raise EncryptedPdfError() + 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 pscript5_mode: + if patcher is not None: patcher.stop() return dev.get_result() diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py new file mode 100644 index 00000000..47de64b6 --- /dev/null +++ b/src/ocrmypdf/pluginspec.py @@ -0,0 +1,445 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from abc import ABC, abstractmethod, abstractstaticmethod +from argparse import ArgumentParser, Namespace +from collections import namedtuple +from logging import Handler +from pathlib import Path +from typing import TYPE_CHECKING, AbstractSet, List, Optional + +import pluggy + +from ocrmypdf._concurrent import Executor +from ocrmypdf.helpers import Resolution + +if TYPE_CHECKING: + from PIL import Image + + # pylint: disable=ungrouped-imports + from ocrmypdf._jobcontext import PageContext + from ocrmypdf.pdfinfo import PdfInfo + + # pylint: enable=ungrouped-imports + +hookspec = pluggy.HookspecMarker('ocrmypdf') + +# pylint: disable=unused-argument + + +@hookspec(firstresult=True) +def get_logging_console() -> Handler: + """Returns a custom logging handler. + + Generally this is necessary when both logging output and a progress bar are both + outputting to ``sys.stderr``. + + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec +def add_options(parser: ArgumentParser) -> None: + """Allows the plugin to add its own command line and API arguments. + + OCRmyPDF converts command line arguments to API arguments, so adding + arguments here will cause new arguments to be processed for API calls + to ``ocrmypdf.ocr``, or when invoked on the command line. + + Note: + This hook will be called from the main process, and may modify global state + before child worker processes are forked. + """ + + +@hookspec +def check_options(options: Namespace) -> None: + """Called to ask the plugin to check all of the options. + + The plugin may check if options that it added are valid. + + Warnings or other messages may be passed to the user by creating a logger + object using ``log = logging.getLogger(__name__)`` and logging to this. + + The plugin may also modify the *options*. All objects that are in options + must be picklable so they can be marshalled to child worker processes. + + Raises: + ocrmypdf.exceptions.ExitCodeException: If options are not acceptable + and the application should terminate gracefully with an informative + message and error code. + Note: + This hook will be called from the main process, and may modify global state + before child worker processes are forked. + """ + + +@hookspec(firstresult=True) +def get_executor(progressbar_class) -> Executor: + """Called to obtain an object that manages parallel execution. + + This may be used to replace OCRmyPDF's default parallel execution system + with a third party alternative. For example, you could make OCRmyPDF run in a + distributed environment. + + OCRmyPDF's executors are analogous to the standard Python executors in + ``conconcurrent.futures``, but they do not work the same way. Executors may + be reused for different, unrelated batch operations, since all of the context + for a given job are passed to :meth:`Executor.__call__`. + + Should be of type :class:`Executor` or otherwise conforming to the protocol + of that call. + + Arguments: + progressbar_class: A progress bar class, which will be created when + + Note: + This hook will be called from the main process, and may modify global state + before child worker processes are forked. + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec(firstresult=True) +def get_progressbar_class(): + """Called to obtain a class that can be used to monitor progress. + + A progress bar is assumed, but this could be used for any type of monitoring. + + The class should follow a tqdm-like protocol. Calling the class should return + a new progress bar object, which is activated with ``__enter__`` and terminated + ``__exit__``. An update method is called whenever the progress bar is updated. + Progress bar objects will not be reused; a new one will be created for each + group of tasks. + + The progress bar is held in the main process/thread and not updated by child + process/threads. When a child notifies the parent of completed work, the + parent updates the progress bar. + + The arguments are the same as `tqdm `_ accepts. + + Progress bars should never write to ``sys.stdout``, or they will corrupt the + output if OCRmyPDF writes a PDF to standard output. + + The type of events that OCRmyPDF reports to a progress bar may change in + minor releases. + + Here is how OCRmyPDF will use the progress bar: + + Example: + pbar_class = pm.hook.get_progressbar_class() + with pbar_class(**tqdm_kwargs) as pbar: + ... + pbar.update(1) + """ + + +@hookspec +def validate(pdfinfo: 'PdfInfo', options: Namespace) -> None: + """Called to give a plugin an opportunity to review *options* and *pdfinfo*. + + *options* contains the "work order" to process a particular file. *pdfinfo* + contains information about the input file obtained after loading and + parsing. The plugin may modify the *options*. For example, you could decide + that a certain type of file should be treated with ``options.force_ocr = True`` + based on information in its *pdfinfo*. + + Raises: + ocrmypdf.exceptions.ExitCodeException: If options or pdfinfo are not acceptable + and the application should terminate gracefully with an informative + message and error code. + Note: + This hook will be called from the main process, and may modify global state + before child worker processes are forked. + """ + + +@hookspec(firstresult=True) +def rasterize_pdf_page( + input_file: Path, + output_file: Path, + raster_device: str, + raster_dpi: Resolution, + pageno: int, + page_dpi: Optional[Resolution], + rotation: Optional[int], + filter_vector: bool, +) -> Path: + """Rasterize one page of a PDF at resolution raster_dpi in canvas units. + + The image is sized to match the integer pixels dimensions implied by + raster_dpi even if those numbers are noninteger. The image's DPI will + be overridden with the values in page_dpi. + + Args: + input_file: The PDF to rasterize. + output_file: The desired name of the rasterized image. + raster_device: Type of image to produce at output_file + raster_dpi: Resolution at which to rasterize page + pageno: Page number to rasterize (beginning at page 1) + page_dpi: Resolution, overriding output image DPI + rotation: Cardinal angle, clockwise, to rotate page + filter_vector: If True, remove vector graphics objects + Returns: + Path: output_file if successful + Note: + This hook will be called from child processes. Modifying global state + will not affect the main process or other child processes. + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec(firstresult=True) +def filter_ocr_image(page: 'PageContext', image: 'Image') -> 'Image': + """Called to filter the image before it is sent to OCR. + + This is the image that OCR sees, not what the user sees when they view the + PDF. If ``redo_ocr`` is enabled, portions of the image will be masked so + they are not shown to OCR. The main use of this hook is expected to be hiding + content from OCR. + + The input image may be color, grayscale, or monochrome, and the + output image may differ. The pixel width and height of the + output image must be identical to the input image, or misalignment between + the OCR text layer and visual position of the text will occur. Likewise, + the output must be a faithful representation of the input, or alignment + errors may occurs. + + Tesseract OCR only deals with monochrome images, and internally converts + non-monochrome images to OCR. + + Note: + This hook will be called from child processes. Modifying global state + will not affect the main process or other child processes. + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec(firstresult=True) +def filter_page_image(page: 'PageContext', image_filename: Path) -> Path: + """Called to filter the whole page before it is inserted into the PDF. + + A whole page image is only produced when preprocessing command line arguments + are issued or when ``--force-ocr`` is issued. If no whole page is image is + produced for a given page, this function will not be called. This is not + the image that will be shown to OCR. + + If the function does not want to modify the image, it should return + ``image_filename``. The hook may overwrite ``image_filename`` with a new file. + + The output image should preserve the same physical unit dimensions, that is + (width * dpi_x, height * dpi_y). That is, if the image is resized, the DPI + must be adjusted by the reciprocal. If this is not preserved, the PDF page + will be resized and the OCR layer misaligned. OCRmyPDF does not nothing + to enforce these constraints; it is up to the plugin to do sensible things. + + OCRmyPDF will create the PDF page based on the image format used (unless the + hook is overriden). If you convert the image to a JPEG, the output page will + be created as a JPEG, etc. If you change the colorspace, that change will be + kept. Note that the OCRmyPDF image optimization stage, if enabled, may + ultimately chose a different format. + + If the return value is a file that does not exist, ``FileNotFoundError`` + will occur. The return value should be a path to a file in the same folder + as ``image_filename``. + + Implementation detail: If the value returned is falsy, OCRmyPDF will ignore + the return value and assume the input file was unmodified. This is deprecated. + To leave the image unmodified, ``image_filename`` should be returned. + + Note: + This hook will be called from child processes. Modifying global state + will not affect the main process or other child processes. + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec(firstresult=True) +def filter_pdf_page( + page: 'PageContext', image_filename: Path, output_pdf: Path +) -> Path: + """Called to convert a filtered whole page image into a PDF. + + A whole page image is only produced when preprocessing command line arguments + are issued or when ``--force-ocr`` is issued. If no whole page is image is + produced for a given page, this function will not be called. This is not + the image that will be shown to OCR. The whole page image is filtered in + the hook above, ``filter_page_image``, then this function is called for + PDF conversion. + + This function will only be called when OCRmyPDF runs in a mode such as + "force OCR" mode where rasterizing of all content is performed. + + Clever things could be done at this stage such as segmenting the page image into + color regions or vector equivalents. + + The provider of the hook implementation is responsible for ensuring that the + OCR text layer is aligned with the PDF produced here, or text misalignment + will result. + + Currently this function must produce a single page PDF or the pipeline will + fail. If the intent is to remove the PDF, then create a single page empty + PDF. + + Args: + page: Context for this page. + image_filename: Filename of the input image used to create output_pdf, + for "reference" if recreating the output_pdf entirely. + output_pdf: The previous created output_pdf. + + Returns: + output_pdf + + Note: + This hook will be called from child processes. Modifying global state + will not affect the main process or other child processes. + Note: + This is a :ref:`firstresult hook`. + """ + + +OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence')) +"""Expresses an OCR engine's confidence in page rotation. + +Attributes: + angle (int): The clockwise angle (0, 90, 180, 270) that the page should be + rotated. 0 means no rotation. + confidence (float): How confident the OCR engine is that this the correct + rotation. 0 is not confident, 15 is very confident. Arbitrary units. +""" + + +class OcrEngine(ABC): + """A class representing an OCR engine with capabilities similar to Tesseract OCR. + + This could be used to create a plugin for another OCR engine instead of + Tesseract OCR. + """ + + @abstractstaticmethod + def version() -> str: + """Returns the version of the OCR engine.""" + + @abstractstaticmethod + def creator_tag(options: Namespace) -> str: + """Returns the creator tag to identify this software's role in creating the PDF. + + This tag will be inserted in the XMP metadata and DocumentInfo dictionary + as appropriate. Ideally you should include the name of the OCR engine and its + version. The text should not contain line breaks. This is to help developers + like yourself identify the software that produced this file. + + OCRmyPDF will always prepend its name to this value. + """ + + @abstractmethod + def __str__(self): + """Returns name of OCR engine and version. + + This is used when OCRmyPDF wants to mention the name of the OCR engine + to the user, usually in an error message. + """ + + @abstractstaticmethod + def languages(options: Namespace) -> AbstractSet[str]: + """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 + can be any value understood by the OCR engine.""" + + @abstractstaticmethod + def get_orientation(input_file: Path, options: Namespace) -> OrientationConfidence: + """Returns the orientation of the image.""" + + @abstractstaticmethod + def generate_hocr( + input_file: Path, output_hocr: Path, output_text: Path, options: Namespace + ) -> None: + """Called to produce a hOCR file and sidecar text file.""" + + @abstractstaticmethod + def generate_pdf( + input_file: Path, output_pdf: Path, output_text: Path, options: Namespace + ) -> None: + """Called to produce a text only PDF. + + Args: + input_file: A page image on which to perform OCR. + output_pdf: The expected name of the output PDF, which must be + a single page PDF with no visible content of any kind, sized + to the dimensions implied by the input_file's width, height + and DPI. The image will be grafted onto the input PDF page. + """ + + +@hookspec(firstresult=True) +def get_ocr_engine() -> OcrEngine: + """Returns an OcrEngine to use for processing this file. + + The OcrEngine may be instantiated multiple times, by both the main process + and child process. As such, it must be obtain store any state in ``options`` + or some common location. + + Note: + This is a :ref:`firstresult hook`. + """ + + +@hookspec(firstresult=True) +def generate_pdfa( + pdf_pages: List[Path], + pdfmark: Path, + output_file: Path, + compression: str, + pdf_version: str, + pdfa_part: str, + progressbar_class, +) -> Path: + """Generate a PDF/A. + + This API strongly assumes a PDF/A generator with Ghostscript's semantics. + + OCRmyPDF will modify the metadata and possibly linearize the PDF/A after it + is generated. + + Arguments: + pdf_pages: A list of one or more filenames, will be merged into output_file. + pdfmark: A PostScript file intended for Ghostscript with details on + how to perform the PDF/A conversion. + output_file: The name of the desired output file. + compression: One of ``'jpeg'``, ``'lossless'``, ``''``. For ``'jpeg'``, + 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. + At its own discretion, the PDF/A generator may raise the version, + but should not lower it. + pdfa_part: The desired PDF/A compliance level, such as ``'2B'``. + progressbar_class: The class of a progress bar with a tqdm-like API. An + instance of this class will be initialized when PDF/A conversion + begins, using + ``instance = progressbar_class(total: int, desc: str, unit:str)``, + defining the number of work units, a user-visible description, + and the name of the work units ("page"). Then ``instance.update()`` + will be called when a work unit is completed. If ``None``, no + progress information is reported. + + Returns: + Path: If successful, the hook should return ``output_file``. + + Note: + This is a :ref:`firstresult hook`. + + See also: + https://github.com/tqdm/tqdm + """ diff --git a/src/ocrmypdf/py.typed b/src/ocrmypdf/py.typed new file mode 100644 index 00000000..0a417894 --- /dev/null +++ b/src/ocrmypdf/py.typed @@ -0,0 +1 @@ +# ocrmypdf is typed diff --git a/src/ocrmypdf/quality.py b/src/ocrmypdf/quality.py new file mode 100644 index 00000000..dab816b0 --- /dev/null +++ b/src/ocrmypdf/quality.py @@ -0,0 +1,50 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Utilities to measure OCR quality""" + + +import re +from typing import Iterable + + +class OcrQualityDictionary: + """Manages a dictionary for simple OCR quality checks.""" + + def __init__(self, *, wordlist: Iterable[str]): + """Construct a dictionary from a list of words. + + Words for which capitalization is important should be capitalized in the + dictionary. Words that contain spaces or other punctuation will never match. + """ + self.dictionary = set(wordlist) + + def measure_words_matched(self, ocr_text: str) -> float: + """Check how many unique words in the OCR text match a dictionary. + + Words with mixed capitalized are only considered a match if the test word + matches that capitalization. + + Returns: + number of words that match / number + """ + text = re.sub(r"[0-9_]+", ' ', ocr_text) + text = re.sub(r'\W+', ' ', text) + text_words_list = re.split(r'\s+', text) + text_words = {w for w in text_words_list if len(w) >= 3} + + matches = 0 + for w in text_words: + if w in self.dictionary or ( + w != w.lower() and w.lower() in self.dictionary + ): + matches += 1 + if matches > 0: + hit_ratio = matches / len(text_words) + else: + hit_ratio = 0.0 + return hit_ratio diff --git a/src/ocrmypdf/subprocess/__init__.py b/src/ocrmypdf/subprocess/__init__.py new file mode 100644 index 00000000..99f052c6 --- /dev/null +++ b/src/ocrmypdf/subprocess/__init__.py @@ -0,0 +1,318 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Wrappers to manage subprocess calls""" + +import logging +import os +import re +import sys +from collections.abc import Mapping +from contextlib import suppress +from distutils.version import LooseVersion, Version +from functools import lru_cache +from pathlib import Path +from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen +from subprocess import run as subprocess_run +from typing import Callable, Optional, Type, Union + +from ocrmypdf.exceptions import MissingDependencyError + +# pylint: disable=logging-format-interpolation + +log = logging.getLogger(__name__) + + +def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs): + """Wrapper around :py:func:`subprocess.run` + + The main purpose of this wrapper is to log subprocess output in an orderly + fashion that indentifies the responsible subprocess. An additional + task is that this function goes to greater lengths to find possible Windows + locations of our dependencies when they are not on the system PATH. + + Arguments should be identical to ``subprocess.run``, except for following: + + Arguments: + logs_errors_to_stdout: If True, indicates that the process writes its error + messages to stdout rather than stderr, so stdout should be logged + if there is an error. If False, stderr is logged. Could be used with + stderr=STDOUT, stdout=PIPE for example. + """ + args, env, process_log, _text = _fix_process_args(args, env, kwargs) + + stderr = None + stderr_name = 'stderr' if not logs_errors_to_stdout else 'stdout' + try: + proc = subprocess_run(args, env=env, **kwargs) + except CalledProcessError as e: + stderr = getattr(e, stderr_name, None) + raise + else: + stderr = getattr(proc, stderr_name, None) + finally: + if process_log.isEnabledFor(logging.DEBUG) and stderr: + with suppress(AttributeError, UnicodeDecodeError): + stderr = stderr.decode('utf-8', 'replace') + if logs_errors_to_stdout: + process_log.debug("stdout/stderr = %s", stderr) + else: + process_log.debug("stderr = %s", stderr) + return proc + + +def run_polling_stderr(args, *, callback, check=False, env=None, **kwargs): + """Run a process like ``ocrmypdf.subprocess.run``, and poll stderr. + + Every line of produced by stderr will be forwarded to the callback function. + The intended use is monitoring progress of subprocesses that output their + own progress indicators. In addition, each line will be logged if debug + logging is enabled. + + Requires stderr to be opened in text mode for ease of handling errors. In + addition the expected encoding= and errors= arguments should be set. Note + that if stdout is already set up, it need not be binary. + """ + args, env, process_log, text = _fix_process_args(args, env, kwargs) + assert text, "Must use text=True" + + with Popen(args, env=env, **kwargs) as proc: + lines = [] + while proc.poll() is None: + for msg in iter(proc.stderr.readline, ''): + if process_log.isEnabledFor(logging.DEBUG): + process_log.debug(msg.strip()) + callback(msg) + lines.append(msg) + stderr = ''.join(lines) + + if check and proc.returncode != 0: + raise CalledProcessError(proc.returncode, args, output=None, stderr=stderr) + return CompletedProcess(args, proc.returncode, None, stderr=stderr) + + +def _fix_process_args(args, env, kwargs): + assert 'universal_newlines' not in kwargs, "Use text= instead of universal_newlines" + + if not env: + env = os.environ + + # Search in spoof path if necessary + program = args[0] + + if os.name == 'nt': + from ocrmypdf.subprocess._windows import fix_windows_args + + args = fix_windows_args(program, args, env) + + log.debug("Running: %s", args) + process_log = log.getChild(os.path.basename(program)) + text = kwargs.get('text', False) + if sys.version_info < (3, 7): + if os.name == 'nt': + # Can't use close_fds=True on Windows with Python 3.6 or older + # https://bugs.python.org/issue19575, etc. + kwargs['close_fds'] = False + if 'text' in kwargs: + # Convert run(...text=) to run(...universal_newlines=) for Python 3.6 + kwargs['universal_newlines'] = kwargs['text'] + del kwargs['text'] + return args, env, process_log, text + + +@lru_cache(maxsize=None) +def get_version( + program: str, *, version_arg: str = '--version', regex=r'(\d+(\.\d+)*)', env=None +): + """Get the version of the specified program + + Arguments: + program: The program to version check. + version_arg: The argument needed to ask for its version, e.g. ``--version``. + regex: A regular expression to parse the program's output and obtain the + version. + env: Custom ``os.environ`` in which to run program. + """ + args_prog = [program, version_arg] + try: + proc = run( + args_prog, + close_fds=True, + text=True, + stdout=PIPE, + stderr=STDOUT, + check=True, + env=env, + ) + output = proc.stdout + except FileNotFoundError as e: + raise MissingDependencyError( + f"Could not find program '{program}' on the PATH" + ) from e + except CalledProcessError as e: + if e.returncode != 0: + raise MissingDependencyError( + f"Ran program '{program}' but it exited with an error:\n{e.output}" + ) from e + raise MissingDependencyError( + f"Could not find program '{program}' on the PATH" + ) from e + + match = re.match(regex, output.strip()) + if not match: + raise MissingDependencyError( + f"The program '{program}' did not report its version. " + f"Message was:\n{output}" + ) + version = match.group(1) + + return version + + +missing_program = ''' +The program '{program}' could not be executed or was not found on your +system PATH. +''' + +missing_optional_program = ''' +The program '{program}' could not be executed or was not found on your +system PATH. This program is required when you use the +{required_for} arguments. You could try omitting these arguments, or install +the package. +''' + +missing_recommend_program = ''' +The program '{program}' could not be executed or was not found on your +system PATH. This program is recommended when using the {required_for} arguments, +but not required, so we will proceed. For best results, install the program. +''' + +old_version = ''' +OCRmyPDF requires '{program}' {need_version} or higher. Your system appears +to have {found_version}. Please update this program. +''' + +old_version_required_for = ''' +OCRmyPDF requires '{program}' {need_version} or higher when run with the +{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to +proceed. For best results, install the program. +''' + +osx_install_advice = ''' +If you have homebrew installed, try these command to install the missing +package: + brew install {package} +''' + +linux_install_advice = ''' +On systems with the aptitude package manager (Debian, Ubuntu), try these +commands: + sudo apt-get update + sudo apt-get install {package} + +On RPM-based systems (Red Hat, Fedora), search for instructions on +installing the RPM for {program}. +''' + +windows_install_advice = ''' +If not already installed, install the Chocolatey package manager. Then use +a command prompt to install the missing package: + choco install {package} +''' + + +def _get_platform(): + if sys.platform.startswith('freebsd'): + return 'freebsd' + elif sys.platform.startswith('linux'): + return 'linux' + elif sys.platform.startswith('win'): + return 'windows' + return sys.platform + + +def _error_trailer(program, package, **kwargs): + if isinstance(package, Mapping): + package = package.get(_get_platform(), program) + + if _get_platform() == 'darwin': + log.info(osx_install_advice.format(**locals())) + elif _get_platform() == 'linux': + log.info(linux_install_advice.format(**locals())) + elif _get_platform() == 'windows': + log.info(windows_install_advice.format(**locals())) + + +def _error_missing_program(program, package, required_for, recommended): + if recommended: + log.warning(missing_recommend_program.format(**locals())) + elif required_for: + log.error(missing_optional_program.format(**locals())) + else: + log.error(missing_program.format(**locals())) + _error_trailer(**locals()) + + +def _error_old_version(program, package, need_version, found_version, required_for): + if required_for: + log.error(old_version_required_for.format(**locals())) + else: + log.error(old_version.format(**locals())) + _error_trailer(**locals()) + + +def check_external_program( + *, + program: str, + package: str, + version_checker: Union[str, Callable], + need_version: str, + required_for: Optional[str] = None, + recommended=False, + version_parser: Type[Version] = LooseVersion, +): + """Check for required version of external program and raise exception if not. + + Args: + program: The name of the program to test. + package: The name of a software package that typically supplies this program. + Usually the same as program. + version_check: A callable without arguments that retrieves the installed + version of program. + need_version: The minimum required version. + required_for: The name of an argument of feature that requires this program. + recommended: If this external program is recommended, instead of raising + an exception, log a warning and allow execution to continue. + version_parser: A class that should be used to parse and compare version + numbers. Used when version numbers do not follow standard conventions. + """ + + try: + if callable(version_checker): + found_version = version_checker() + else: + found_version = version_checker + except (CalledProcessError, FileNotFoundError, MissingDependencyError): + _error_missing_program(program, package, required_for, recommended) + if not recommended: + raise MissingDependencyError(program) + return + + def remove_leading_v(s): + if s.startswith('v'): + return s[1:] + return s + + found_version = remove_leading_v(found_version) + need_version = remove_leading_v(need_version) + + 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: + raise MissingDependencyError(program) + + log.debug('Found %s %s', program, found_version) diff --git a/src/ocrmypdf/subprocess/_windows.py b/src/ocrmypdf/subprocess/_windows.py new file mode 100644 index 00000000..aec082b7 --- /dev/null +++ b/src/ocrmypdf/subprocess/_windows.py @@ -0,0 +1,162 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging +import os +import shutil +import sys +from distutils.version import LooseVersion +from itertools import chain, filterfalse +from pathlib import Path +from typing import Any, Callable, Iterator, Optional, Tuple, TypeVar, cast + +try: + import winreg +except ModuleNotFoundError as e: + raise ModuleNotFoundError("This module is for Windows only") from e + +log = logging.getLogger(__name__) + +T = TypeVar('T') + + +def registry_enum( + key: winreg.HKEYType, enum_fn: Callable[[winreg.HKEYType, int], T] +) -> Iterator[T]: + LIMIT = 999 + n = 0 + while n < LIMIT: + try: + yield enum_fn(key, n) + n += 1 + except OSError: + break + if n == LIMIT: + raise ValueError(f"Too many registry keys under {key}") + + +def registry_subkeys(key: winreg.HKEYType) -> Iterator[str]: + return registry_enum(key, winreg.EnumKey) + + +def registry_values(key: winreg.HKEYType) -> Iterator[Tuple[str, Any, int]]: + return registry_enum(key, winreg.EnumValue) + + +def registry_path_ghostscript(env=None) -> Iterator[Path]: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Artifex\GPL Ghostscript" + ) as k: + latest_gs = max(registry_subkeys(k), key=LooseVersion) + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}" + ) as k: + _, gs_path, _ = next(registry_values(k)) + yield Path(gs_path) / 'bin' + except OSError as e: + log.warning(e) + + +def registry_path_tesseract(env=None) -> Iterator[Path]: + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Tesseract-OCR") as k: + for subkey, val, _valtype in registry_values(k): + if subkey == 'InstallDir': + tesseract_path = Path(val) + yield tesseract_path + except OSError as e: + log.warning(e) + + +def program_files_paths(env=None) -> Iterator[Path]: + if not env: + env = os.environ + program_files = env.get('PROGRAMFILES', '') + + def path_walker() -> Iterator[Path]: + for path in Path(program_files).iterdir(): + if not path.is_dir(): + continue + if path.name.lower() == 'tesseract-ocr': + yield path + elif path.name.lower() == 'gs': + yield from (p for p in path.glob('**/bin') if p.is_dir()) + + return iter( + sorted( + (p for p in path_walker()), + key=lambda p: (p.name, p.parent.name), + reverse=True, + ) + ) + + +def paths_from_env(env=None) -> Iterator[Path]: + return (Path(p) for p in os.get_exec_path(env) if p) + + +def shim_path(new_paths: Callable[[Any], Iterator[Path]], env=None) -> str: + if not env: + env = os.environ + return os.pathsep.join(str(p) for p in new_paths(env) if p) + + +SHIMS = [ + paths_from_env, + registry_path_ghostscript, + registry_path_tesseract, + program_files_paths, +] + + +def fix_windows_args(program, args, env): + """Adjust our desired program and command line arguments for use on Windows""" + + if sys.version_info < (3, 8): + # bpo-33617 - Windows needs manual Path -> str conversion + args = [os.fspath(arg) for arg in args] + program = os.fspath(program) + + # If we are running a .py on Windows, ensure we call it with this Python + # (to support test suite shims) + if program.lower().endswith('.py'): + args = [sys.executable] + args + + # If the program we want is not on the PATH, check elsewhere + for shim in SHIMS: + shimmed_path = shim_path(shim, env) + new_args0 = shutil.which(args[0], path=shimmed_path) + if new_args0: + args[0] = new_args0 + break + + return args + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + key = lambda x: x + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element + + +def shim_env_path(env=None): + if env is None: + env = os.environ + + shim_paths = chain.from_iterable(shim(env) for shim in SHIMS) + return os.pathsep.join( + str(p) for p in unique_everseen(shim_paths, key=lambda p: str.casefold(str(p))) + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..f714d74d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Empty __init__.py file diff --git a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 64% rename from tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index 5bf76e48..023d341f 100644 Binary files a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/2400dpi/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 73% rename from tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index 7cedf01c..edfe4b01 100644 Binary files a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..151bced2 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,30 @@ +Tarnose + + + + + + +Bokale oa + + + +Lehuntze + + + + + +Mugerre + + + + +Milafranga Komunikabideak + +BAIONA i zeettnansise — + +1 Trenbideak -- ~~~ + +t\ Basusarri — spmsans20141004 se: . a ~ + \ No newline at end of file diff --git a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin similarity index 85% rename from tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin rename to tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin index 13be989c..22b7068b 100644 Binary files a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin and b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..2df091b8 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,2 @@ +Covfefe is a perfectly cromulent word. + \ No newline at end of file diff --git a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin similarity index 63% rename from tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin index 05592c58..0bb07c30 100644 Binary files a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..522e4174 --- /dev/null +++ b/tests/cache/3small/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,27 @@ +Linzensoep a la Waterman + + + +4 ons linzen + +3 liter water + +3 uien + +bloem, boter + +2 kopjes melk + +laurier, kruidnagel, kerrie, zout + +De linzgen wassen en in -l liter kokend wa- +ter 1 dag laten weken, 2 liter water bij +de linzen voegen, zonder het water waarin +ze geweekt zijn af te gieten, De helft van +de uien bakken met laurier en Kruicdnagel. +Alle uien, kerrie en gout bij de linzen +voegen, Alles aan de kook brengen,. Van de +bloem met boter en melk een papje maken en +verder afmaken met de soep, Als de linzen +gaar Zijn is de soep klaar. + \ No newline at end of file diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 66% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index bc723882..4a5241fc 100644 --- a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,28 +4,28 @@ - - + + - - -
+ + +

- +

- This - should - be - a - perfect - circle: + This + should + be + a + perfect + circle:

diff --git a/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 87% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index 4420bebc..a0e93b41 100644 Binary files a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/aspect/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index e308fb4c..00000000 Binary files a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin deleted file mode 100644 index d3c2e860..00000000 --- a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin +++ /dev/null @@ -1,123 +0,0 @@ -The LinnSequencer -32 Track MIDI Sequence Recorder - -The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is - -extremely powerful, yet amazingly simple to learn and use. It’s many remarkable features include: - -¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST -FORWARD, REWIND, and LOCATE controls. - -e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may -be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic - -synthesizers! - -¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes - -per disk! - -¢ One or all tracks may be TRANSPOSED at the touch of a key. -e Exclusive real-time ERASE function makes editing FAST. -* Exclusive REPEAT function automatically repeats any held notes at a pre-selected - -rhythmic value. - -¢ TIMING CORRECTION works during playback and operates without ‘chopping’ notes. - -¢ Optional SMPTE time code synchronization. - -© Optional remote control. - -Recording a Sequence - -To record a sequence, simply press RECORD and PLAY, -then play your MIDI keyboard in time to the Sequencer’s -click track. When the sequence loops back around to bar 1, -you’ ll hear what you played—only all timing errors will be - -corrected! (Timing correction may be adjusted or defeated). - -Any additional notes played will be added into the track -— existing notes are not erased while recording! - -FAST FORWARD, REWIND, and LOCATE controls -may be used at any time to quickly access any location in -your sequence for spot-recording. To overdub a new part, -select a different track and start recording—while you -record, the first track will play in perfect sync (unless you -MUTE it, or SOLO another track). In this way, up to 32 -tracks may be overdubbed! All MIDI effects are recorded -including pitch bend, modulation, velocity, aftertouch, -sustain pedal, and program changes! - -Editing - -To erase a wrong note, simply hold ERASE and press -the note to be erased just before it plays in the sequence— -when played back, it will be gone. Notes may also be - -added, erased, or changed using the SINGLE STEP func- -tion. To overdub notes at specific points within a sequence, - -Additional Features - -simply use LOCATE, FAST FORWARD, or REWIND to -find the desired bar number, then start recording. - -The INSERT/COPY function allows you to move bars -from one location to another—in the same sequence or a -different one. For example, you might insert a copy of the -first verse between the second chorus and the bridge. -DELETE BARS operates the same way to remove -unwanted sections, - -Creating a Song - -One way to create a song is to record each track all the -way through (up to 999 bars). Another way is to record -each basic section (verse, chorus, etc.) in individual -sequences, then use the CREATE SONG function to “chain” -them together. CREATE SONG will then automatically -copy all the parts into a new sequence. If desired, you can -even set the last few bars to repeat infinitely, for a fadeout. - -Composition Without Compromise - -The technology you use should never be so complex that -it interferes with the creative process. That’s precisely why -the LinnSequencer is designed to let you compose, record -and edit while devoting your undivided attention to your -music. See your Linn dealer today for a demonstration! - -* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the - -HELP button displays additional explanations. - -* Non-destructive recording—existing notes are not erased while recording. -¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including - -ERASE, REPEAT, PLAY/STOP, or LOCATE. - -¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value. - -© Will sync to standard LinnDrum or Linn 9000 sync tone. - -© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. -* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second, - -(even drop frame!) - -¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes - -on the TAP TEMPO button. - -¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired. -¢ Any TIME SIGNATURE may be used, and may be changed within a song. - -linn -Linn Electronics, Inc. - -18720 Oxnard Street, Tarzana, CA 91356 -(818) 708-8131 TELEX #298949 LINN UR - \ No newline at end of file diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 51% rename from tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin rename to tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index a4a254f1..51333eae 100644 --- a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin +++ b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,1059 +4,1059 @@ - - + + - - -
+ + +

- - The - LinnSequencer + + The + LinnSequencer - - 32 - Track - MIDI - Sequence - Recorder + + 32 + Track + MIDI + Sequence + Recorder

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is + + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is

- - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It’s - many - remarkable - features - include: + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It’s + many + remarkable + features + include:

- - ¢ - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST + + ¢ + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST - - FORWARD, - REWIND, - and - LOCATE - controls. + + FORWARD, + REWIND, + and + LOCATE + controls.

- - e - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may + + e + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic

- synthesizers! + synthesizers!

- - ¢ - Ultra-fast - 3%” - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes + + ¢ + Ultra-fast + 3%” + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes

- per - disk! + per + disk!

- - ¢ - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. + + ¢ + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. - - e - Exclusive - real-time - ERASE - function - makes - editing - FAST. + + e + Exclusive + real-time + ERASE + function + makes + editing + FAST. - - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected + + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected

- rhythmic - value. + rhythmic + value.

- - ¢ - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes. + + ¢ + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes.

- - ¢ - Optional - SMPTE - time - code - synchronization. + + ¢ + Optional + SMPTE + time + code + synchronization.

- © - Optional - remote - control. + © + Optional + remote + control.

- Recording - a - Sequence + Recording + a + Sequence

- To - record - a - sequence, - simply - press - RECORD - and - PLAY, + To + record + a + sequence, + simply + press + RECORD + and + PLAY, - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s - click - track. - When - the - sequence - loops - back - around - to - bar - 1, + click + track. + When + the + sequence + loops + back + around + to + bar + 1, - you’ - ll - hear - what - you - played—only - all - timing - errors - will - be + you’ + ll + hear + what + you + played—only + all + timing + errors + will + be

- - corrected! - (Timing - correction - may - be - adjusted - or - defeated). + + corrected! + (Timing + correction + may + be + adjusted + or + defeated).

- Any - additional - notes - played - will - be - added - into - the - track + Any + additional + notes + played + will + be + added + into + the + track - — - existing - notes - are - not - erased - while - recording! + — + existing + notes + are + not + erased + while + recording!

- FAST - FORWARD, - REWIND, - and - LOCATE - controls + FAST + FORWARD, + REWIND, + and + LOCATE + controls - may - be - used - at - any - time - to - quickly - access - any - location - in + may + be + used + at + any + time + to + quickly + access + any + location + in - your - sequence - for - spot-recording. - To - overdub - a - new - part, + your + sequence + for + spot-recording. + To + overdub + a + new + part, - select - a - different - track - and - start - recording—while - you + select + a + different + track + and + start + recording—while + you - record, - the - first - track - will - play - in - perfect - sync - (unless - you + record, + the + first + track + will + play + in + perfect + sync + (unless + you - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded - including - pitch - bend, - modulation, - velocity, - aftertouch, + including + pitch + bend, + modulation, + velocity, + aftertouch, - sustain - pedal, - and - program - changes! + sustain + pedal, + and + program + changes!

- Editing + Editing

- To - erase - a - wrong - note, - simply - hold - ERASE - and - press + To + erase + a + wrong + note, + simply + hold + ERASE + and + press - the - note - to - be - erased - just - before - it - plays - in - the - sequence— + the + note + to + be + erased + just + before + it + plays + in + the + sequence— - when - played - back, - it - will - be - gone. - Notes - may - also - be + when + played + back, + it + will + be + gone. + Notes + may + also + be

- added, - erased, - or - changed - using - the - SINGLE - STEP - func- + added, + erased, + or + changed + using + the + SINGLE + STEP + func- - tion. - To - overdub - notes - at - specific - points - within - a - sequence, + tion. + To + overdub + notes + at + specific + points + within + a + sequence,

- Additional - Features + Additional + Features

- simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to - find - the - desired - bar - number, - then - start - recording. + find + the + desired + bar + number, + then + start + recording.

- The - INSERT/COPY - function - allows - you - to - move - bars + The + INSERT/COPY + function + allows + you + to + move + bars - from - one - location - to - another—in - the - same - sequence - or - a + from + one + location + to + another—in + the + same + sequence + or + a - different - one. - For - example, - you - might - insert - a - copy - of - the + different + one. + For + example, + you + might + insert + a + copy + of + the - first - verse - between - the - second - chorus - and - the - bridge. + first + verse + between + the + second + chorus + and + the + bridge. - DELETE - BARS - operates - the - same - way - to - remove + DELETE + BARS + operates + the + same + way + to + remove - unwanted - sections, + unwanted + sections,

- Creating - a - Song + Creating + a + Song

- One - way - to - create - a - song - is - to - record - each - track - all - the + One + way + to + create + a + song + is + to + record + each + track + all + the - way - through - (up - to - 999 - bars). - Another - way - is - to - record + way + through + (up + to + 999 + bars). + Another + way + is + to + record - each - basic - section - (verse, - chorus, - etc.) - in - individual + each + basic + section + (verse, + chorus, + etc.) + in + individual - sequences, - then - use - the - CREATE - SONG - function - to - “chain” + sequences, + then + use + the + CREATE + SONG + function + to + “chain” - them - together. - CREATE - SONG - will - then - automatically + them + together. + CREATE + SONG + will + then + automatically - copy - all - the - parts - into - a - new - sequence. - If - desired, - you - can + copy + all + the + parts + into + a + new + sequence. + If + desired, + you + can - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout.

- Composition - Without - Compromise + Composition + Without + Compromise

- The - technology - you - use - should - never - be - so - complex - that + The + technology + you + use + should + never + be + so + complex + that - it - interferes - with - the - creative - process. - That’s - precisely - why + it + interferes + with + the + creative + process. + That’s + precisely + why - the - LinnSequencer - is - designed - to - let - you - compose, - record + the + LinnSequencer + is + designed + to + let + you + compose, + record - and - edit - while - devoting - your - undivided - attention - to - your + and + edit + while + devoting + your + undivided + attention + to + your - music. - See - your - Linn - dealer - today - for - a - demonstration! + music. + See + your + Linn + dealer + today + for + a + demonstration!

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations. - If - needed, - the + + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations. + If + needed, + the

- HELP - button - displays - additional - explanations. + HELP + button + displays + additional + explanations.

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. + + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. - - ¢ - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including + + ¢ + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including

- ERASE, - REPEAT, - PLAY/STOP, - or - LOCATE. + ERASE, + REPEAT, + PLAY/STOP, + or + LOCATE.

- - ¢ - Iwo - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. + + ¢ + Iwo + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value.

- © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone. + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone.

- - © - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. + + © + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. - - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, + + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second,

- (even - drop - frame!) + (even + drop + frame!)

- - ¢ - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes + + ¢ + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes

- on - the - TAP - TEMPO - button. + on + the + TAP + TEMPO + button.

- - ¢ - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. + + ¢ + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. - - ¢ - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. + + ¢ + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song.

- linn + linn - Linn - Electronics, - Inc. + 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

diff --git a/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..394e7bbd Binary files /dev/null and b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin deleted file mode 100644 index e308fb4c..00000000 Binary files a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin deleted file mode 100644 index d3c2e860..00000000 --- a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin +++ /dev/null @@ -1,123 +0,0 @@ -The LinnSequencer -32 Track MIDI Sequence Recorder - -The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is - -extremely powerful, yet amazingly simple to learn and use. It’s many remarkable features include: - -¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST -FORWARD, REWIND, and LOCATE controls. - -e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may -be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic - -synthesizers! - -¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes - -per disk! - -¢ One or all tracks may be TRANSPOSED at the touch of a key. -e Exclusive real-time ERASE function makes editing FAST. -* Exclusive REPEAT function automatically repeats any held notes at a pre-selected - -rhythmic value. - -¢ TIMING CORRECTION works during playback and operates without ‘chopping’ notes. - -¢ Optional SMPTE time code synchronization. - -© Optional remote control. - -Recording a Sequence - -To record a sequence, simply press RECORD and PLAY, -then play your MIDI keyboard in time to the Sequencer’s -click track. When the sequence loops back around to bar 1, -you’ ll hear what you played—only all timing errors will be - -corrected! (Timing correction may be adjusted or defeated). - -Any additional notes played will be added into the track -— existing notes are not erased while recording! - -FAST FORWARD, REWIND, and LOCATE controls -may be used at any time to quickly access any location in -your sequence for spot-recording. To overdub a new part, -select a different track and start recording—while you -record, the first track will play in perfect sync (unless you -MUTE it, or SOLO another track). In this way, up to 32 -tracks may be overdubbed! All MIDI effects are recorded -including pitch bend, modulation, velocity, aftertouch, -sustain pedal, and program changes! - -Editing - -To erase a wrong note, simply hold ERASE and press -the note to be erased just before it plays in the sequence— -when played back, it will be gone. Notes may also be - -added, erased, or changed using the SINGLE STEP func- -tion. To overdub notes at specific points within a sequence, - -Additional Features - -simply use LOCATE, FAST FORWARD, or REWIND to -find the desired bar number, then start recording. - -The INSERT/COPY function allows you to move bars -from one location to another—in the same sequence or a -different one. For example, you might insert a copy of the -first verse between the second chorus and the bridge. -DELETE BARS operates the same way to remove -unwanted sections, - -Creating a Song - -One way to create a song is to record each track all the -way through (up to 999 bars). Another way is to record -each basic section (verse, chorus, etc.) in individual -sequences, then use the CREATE SONG function to “chain” -them together. CREATE SONG will then automatically -copy all the parts into a new sequence. If desired, you can -even set the last few bars to repeat infinitely, for a fadeout. - -Composition Without Compromise - -The technology you use should never be so complex that -it interferes with the creative process. That’s precisely why -the LinnSequencer is designed to let you compose, record -and edit while devoting your undivided attention to your -music. See your Linn dealer today for a demonstration! - -* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the - -HELP button displays additional explanations. - -* Non-destructive recording—existing notes are not erased while recording. -¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including - -ERASE, REPEAT, PLAY/STOP, or LOCATE. - -¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value. - -© Will sync to standard LinnDrum or Linn 9000 sync tone. - -© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. -* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second, - -(even drop frame!) - -¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes - -on the TAP TEMPO button. - -¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired. -¢ Any TIME SIGNATURE may be used, and may be changed within a song. - -linn -Linn Electronics, Inc. - -18720 Oxnard Street, Tarzana, CA 91356 -(818) 708-8131 TELEX #298949 LINN UR - \ No newline at end of file diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/hocr.bin similarity index 51% rename from tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/hocr.bin index ea557687..23a18626 100644 --- a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/hocr.bin @@ -4,1059 +4,1059 @@ - - + + - - -
+ + +

- - The - LinnSequencer + + The + LinnSequencer - - 32 - Track - MIDI - Sequence - Recorder + + 32 + Track + MIDI + Sequence + Recorder

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is + + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is

- - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It’s - many - remarkable - features - include: + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It’s + many + remarkable + features + include:

- - ¢ - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST + + ¢ + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST - - FORWARD, - REWIND, - and - LOCATE - controls. + + FORWARD, + REWIND, + and + LOCATE + controls.

- - e - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may + + e + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic

- synthesizers! + synthesizers!

- - ¢ - Ultra-fast - 3%” - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes + + ¢ + Ultra-fast + 3%” + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes

- per - disk! + per + disk!

- - ¢ - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. + + ¢ + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. - - e - Exclusive - real-time - ERASE - function - makes - editing - FAST. + + e + Exclusive + real-time + ERASE + function + makes + editing + FAST. - - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected + + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected

- rhythmic - value. + rhythmic + value.

- - ¢ - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes. + + ¢ + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes.

- - ¢ - Optional - SMPTE - time - code - synchronization. + + ¢ + Optional + SMPTE + time + code + synchronization.

- © - Optional - remote - control. + © + Optional + remote + control.

- Recording - a - Sequence + Recording + a + Sequence

- To - record - a - sequence, - simply - press - RECORD - and - PLAY, + To + record + a + sequence, + simply + press + RECORD + and + PLAY, - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s - click - track. - When - the - sequence - loops - back - around - to - bar - 1, + click + track. + When + the + sequence + loops + back + around + to + bar + 1, - you’ - ll - hear - what - you - played—only - all - timing - errors - will - be + you’ + ll + hear + what + you + played—only + all + timing + errors + will + be

- - corrected! - (Timing - correction - may - be - adjusted - or - defeated). + + corrected! + (Timing + correction + may + be + adjusted + or + defeated).

- Any - additional - notes - played - will - be - added - into - the - track + Any + additional + notes + played + will + be + added + into + the + track - — - existing - notes - are - not - erased - while - recording! + — + existing + notes + are + not + erased + while + recording!

- FAST - FORWARD, - REWIND, - and - LOCATE - controls + FAST + FORWARD, + REWIND, + and + LOCATE + controls - may - be - used - at - any - time - to - quickly - access - any - location - in + may + be + used + at + any + time + to + quickly + access + any + location + in - your - sequence - for - spot-recording. - To - overdub - a - new - part, + your + sequence + for + spot-recording. + To + overdub + a + new + part, - select - a - different - track - and - start - recording—while - you + select + a + different + track + and + start + recording—while + you - record, - the - first - track - will - play - in - perfect - sync - (unless - you + record, + the + first + track + will + play + in + perfect + sync + (unless + you - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded - including - pitch - bend, - modulation, - velocity, - aftertouch, + including + pitch + bend, + modulation, + velocity, + aftertouch, - sustain - pedal, - and - program - changes! + sustain + pedal, + and + program + changes!

- Editing + Editing

- To - erase - a - wrong - note, - simply - hold - ERASE - and - press + To + erase + a + wrong + note, + simply + hold + ERASE + and + press - the - note - to - be - erased - just - before - it - plays - in - the - sequence— + the + note + to + be + erased + just + before + it + plays + in + the + sequence— - when - played - back, - it - will - be - gone. - Notes - may - also - be + when + played + back, + it + will + be + gone. + Notes + may + also + be

- added, - erased, - or - changed - using - the - SINGLE - STEP - func- + added, + erased, + or + changed + using + the + SINGLE + STEP + func- - tion. - To - overdub - notes - at - specific - points - within - a - sequence, + tion. + To + overdub + notes + at + specific + points + within + a + sequence,

- Additional - Features + Additional + Features

- simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to - find - the - desired - bar - number, - then - start - recording. + find + the + desired + bar + number, + then + start + recording.

- The - INSERT/COPY - function - allows - you - to - move - bars + The + INSERT/COPY + function + allows + you + to + move + bars - from - one - location - to - another—in - the - same - sequence - or - a + from + one + location + to + another—in + the + same + sequence + or + a - different - one. - For - example, - you - might - insert - a - copy - of - the + different + one. + For + example, + you + might + insert + a + copy + of + the - first - verse - between - the - second - chorus - and - the - bridge. + first + verse + between + the + second + chorus + and + the + bridge. - DELETE - BARS - operates - the - same - way - to - remove + DELETE + BARS + operates + the + same + way + to + remove - unwanted - sections, + unwanted + sections,

- Creating - a - Song + Creating + a + Song

- One - way - to - create - a - song - is - to - record - each - track - all - the + One + way + to + create + a + song + is + to + record + each + track + all + the - way - through - (up - to - 999 - bars). - Another - way - is - to - record + way + through + (up + to + 999 + bars). + Another + way + is + to + record - each - basic - section - (verse, - chorus, - etc.) - in - individual + each + basic + section + (verse, + chorus, + etc.) + in + individual - sequences, - then - use - the - CREATE - SONG - function - to - “chain” + sequences, + then + use + the + CREATE + SONG + function + to + “chain” - them - together. - CREATE - SONG - will - then - automatically + them + together. + CREATE + SONG + will + then + automatically - copy - all - the - parts - into - a - new - sequence. - If - desired, - you - can + copy + all + the + parts + into + a + new + sequence. + If + desired, + you + can - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout.

- Composition - Without - Compromise + Composition + Without + Compromise

- The - technology - you - use - should - never - be - so - complex - that + The + technology + you + use + should + never + be + so + complex + that - it - interferes - with - the - creative - process. - That’s - precisely - why + it + interferes + with + the + creative + process. + That’s + precisely + why - the - LinnSequencer - is - designed - to - let - you - compose, - record + the + LinnSequencer + is + designed + to + let + you + compose, + record - and - edit - while - devoting - your - undivided - attention - to - your + and + edit + while + devoting + your + undivided + attention + to + your - music. - See - your - Linn - dealer - today - for - a - demonstration! + music. + See + your + Linn + dealer + today + for + a + demonstration!

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations. - If - needed, - the + + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations. + If + needed, + the

- HELP - button - displays - additional - explanations. + HELP + button + displays + additional + explanations.

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. + + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. - - ¢ - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including + + ¢ + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including

- ERASE, - REPEAT, - PLAY/STOP, - or - LOCATE. + ERASE, + REPEAT, + PLAY/STOP, + or + LOCATE.

- - ¢ - Iwo - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. + + ¢ + Iwo + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value.

- © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone. + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone.

- - © - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. + + © + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. - - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, + + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second,

- (even - drop - frame!) + (even + drop + frame!)

- - ¢ - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes + + ¢ + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes

- on - the - TAP - TEMPO - button. + on + the + TAP + TEMPO + button.

- - ¢ - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. + + ¢ + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. - - ¢ - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. + + ¢ + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song.

- linn + linn - Linn - Electronics, - Inc. + 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

diff --git a/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..dd362045 Binary files /dev/null and b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin similarity index 98% rename from tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin index d3c2e860..686fd1ac 100644 --- a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin +++ b/tests/cache/cardinal/__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt/txt.bin @@ -103,7 +103,7 @@ ERASE, REPEAT, PLAY/STOP, or LOCATE. © Will sync to standard LinnDrum or Linn 9000 sync tone. -© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. +® Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. * TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second, (even drop frame!) @@ -115,9 +115,9 @@ on the TAP TEMPO button. ¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired. ¢ Any TIME SIGNATURE may be used, and may be changed within a song. -linn -Linn Electronics, Inc. +nn +Linn Electronics, Inc. 18720 Oxnard Street, Tarzana, CA 91356 (818) 708-8131 TELEX #298949 LINN UR \ No newline at end of file diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin deleted file mode 100644 index e308fb4c..00000000 Binary files a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/hocr.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin similarity index 51% rename from tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/hocr.bin rename to tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin index 22521688..48bd2afa 100644 --- a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/hocr.bin +++ b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin @@ -4,1059 +4,1059 @@ - - + + - - -
+ + +

- - The - LinnSequencer + + The + LinnSequencer - - 32 - Track - MIDI - Sequence - Recorder + + 32 + Track + MIDI + Sequence + Recorder

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is + + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is

- - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It’s - many - remarkable - features - include: + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It’s + many + remarkable + features + include:

- - ¢ - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST + + ¢ + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST - - FORWARD, - REWIND, - and - LOCATE - controls. + + FORWARD, + REWIND, + and + LOCATE + controls.

- - e - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may + + e + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic

- synthesizers! + synthesizers!

- - ¢ - Ultra-fast - 3%” - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes + + ¢ + Ultra-fast + 3%” + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes

- per - disk! + per + disk!

- - ¢ - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. + + ¢ + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. - - e - Exclusive - real-time - ERASE - function - makes - editing - FAST. + + e + Exclusive + real-time + ERASE + function + makes + editing + FAST. - - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected + + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected

- rhythmic - value. + rhythmic + value.

- - ¢ - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes. + + ¢ + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes.

- - ¢ - Optional - SMPTE - time - code - synchronization. + + ¢ + Optional + SMPTE + time + code + synchronization.

- © - Optional - remote - control. + © + Optional + remote + control.

- Recording - a - Sequence + Recording + a + Sequence

- To - record - a - sequence, - simply - press - RECORD - and - PLAY, + To + record + a + sequence, + simply + press + RECORD + and + PLAY, - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s - click - track. - When - the - sequence - loops - back - around - to - bar - 1, + click + track. + When + the + sequence + loops + back + around + to + bar + 1, - you’ - ll - hear - what - you - played—only - all - timing - errors - will - be + you’ + ll + hear + what + you + played—only + all + timing + errors + will + be

- - corrected! - (Timing - correction - may - be - adjusted - or - defeated). + + corrected! + (Timing + correction + may + be + adjusted + or + defeated).

- Any - additional - notes - played - will - be - added - into - the - track + Any + additional + notes + played + will + be + added + into + the + track - — - existing - notes - are - not - erased - while - recording! + — + existing + notes + are + not + erased + while + recording!

- FAST - FORWARD, - REWIND, - and - LOCATE - controls + FAST + FORWARD, + REWIND, + and + LOCATE + controls - may - be - used - at - any - time - to - quickly - access - any - location - in + may + be + used + at + any + time + to + quickly + access + any + location + in - your - sequence - for - spot-recording. - To - overdub - a - new - part, + your + sequence + for + spot-recording. + To + overdub + a + new + part, - select - a - different - track - and - start - recording—while - you + select + a + different + track + and + start + recording—while + you - record, - the - first - track - will - play - in - perfect - sync - (unless - you + record, + the + first + track + will + play + in + perfect + sync + (unless + you - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded - including - pitch - bend, - modulation, - velocity, - aftertouch, + including + pitch + bend, + modulation, + velocity, + aftertouch, - sustain - pedal, - and - program - changes! + sustain + pedal, + and + program + changes!

- Editing + Editing

- To - erase - a - wrong - note, - simply - hold - ERASE - and - press + To + erase + a + wrong + note, + simply + hold + ERASE + and + press - the - note - to - be - erased - just - before - it - plays - in - the - sequence— + the + note + to + be + erased + just + before + it + plays + in + the + sequence— - when - played - back, - it - will - be - gone. - Notes - may - also - be + when + played + back, + it + will + be + gone. + Notes + may + also + be

- added, - erased, - or - changed - using - the - SINGLE - STEP - func- + added, + erased, + or + changed + using + the + SINGLE + STEP + func- - tion. - To - overdub - notes - at - specific - points - within - a - sequence, + tion. + To + overdub + notes + at + specific + points + within + a + sequence,

- Additional - Features + Additional + Features

- simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to - find - the - desired - bar - number, - then - start - recording. + find + the + desired + bar + number, + then + start + recording.

- The - INSERT/COPY - function - allows - you - to - move - bars + The + INSERT/COPY + function + allows + you + to + move + bars - from - one - location - to - another—in - the - same - sequence - or - a + from + one + location + to + another—in + the + same + sequence + or + a - different - one. - For - example, - you - might - insert - a - copy - of - the + different + one. + For + example, + you + might + insert + a + copy + of + the - first - verse - between - the - second - chorus - and - the - bridge. + first + verse + between + the + second + chorus + and + the + bridge. - DELETE - BARS - operates - the - same - way - to - remove + DELETE + BARS + operates + the + same + way + to + remove - unwanted - sections, + unwanted + sections,

- Creating - a - Song + Creating + a + Song

- One - way - to - create - a - song - is - to - record - each - track - all - the + One + way + to + create + a + song + is + to + record + each + track + all + the - way - through - (up - to - 999 - bars). - Another - way - is - to - record + way + through + (up + to + 999 + bars). + Another + way + is + to + record - each - basic - section - (verse, - chorus, - etc.) - in - individual + each + basic + section + (verse, + chorus, + etc.) + in + individual - sequences, - then - use - the - CREATE - SONG - function - to - “chain” + sequences, + then + use + the + CREATE + SONG + function + to + “chain” - them - together. - CREATE - SONG - will - then - automatically + them + together. + CREATE + SONG + will + then + automatically - copy - all - the - parts - into - a - new - sequence. - If - desired, - you - can + copy + all + the + parts + into + a + new + sequence. + If + desired, + you + can - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout.

- Composition - Without - Compromise + Composition + Without + Compromise

- The - technology - you - use - should - never - be - so - complex - that + The + technology + you + use + should + never + be + so + complex + that - it - interferes - with - the - creative - process. - That’s - precisely - why + it + interferes + with + the + creative + process. + That’s + precisely + why - the - LinnSequencer - is - designed - to - let - you - compose, - record + the + LinnSequencer + is + designed + to + let + you + compose, + record - and - edit - while - devoting - your - undivided - attention - to - your + and + edit + while + devoting + your + undivided + attention + to + your - music. - See - your - Linn - dealer - today - for - a - demonstration! + music. + See + your + Linn + dealer + today + for + a + demonstration!

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations. - If - needed, - the + + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations. + If + needed, + the

- HELP - button - displays - additional - explanations. + HELP + button + displays + additional + explanations.

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. + + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. - - ¢ - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including + + ¢ + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including

- ERASE, - REPEAT, - PLAY/STOP, - or - LOCATE. + ERASE, + REPEAT, + PLAY/STOP, + or + LOCATE.

- - ¢ - Iwo - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. + + ¢ + Iwo + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value.

- © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone. + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone.

- - © - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. + + © + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. - - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, + + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second,

- (even - drop - frame!) + (even + drop + frame!)

- - ¢ - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes + + ¢ + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes

- on - the - TAP - TEMPO - button. + on + the + TAP + TEMPO + button.

- - ¢ - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. + + ¢ + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. - - ¢ - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. + + ¢ + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song.

- linn + linn - Linn - Electronics, - Inc. + 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

diff --git a/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..a9c86e15 Binary files /dev/null and b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..d80b111f --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,128 @@ +2A NNI‘I 6F6867# XATALL IE18-80L (818) + +9SEI6 VO “BUBZIRY, “J0aNS PIPUXO OZLEI +“Uy ‘soTUOMOI,q UUrT + +uut] + +“‘SUOS B UIJIM pasueyo oq ABU pue ‘posn oq AWW AYN IVNOIS AWLL AUV +“parlsop Jr SUOTIISUBI} YIOOUIS YIM “BoueNbas eB OJUI pourtueIZOId 9q ABU SFONWHO OdINAL e + +‘uonng OdNAL dV L 9) uO + +sojou Jayienb Suiddy} Aq 10 ‘syUSTIOIOUI oINUTIAI-J8g-Jesg & JO sys} UL ofquisn(pe ‘ATTeouIAUINU paiajus oq ABU OdINALL e + +(jouer doup u3a9) + +“puooes Jed souely O€ 10 “SZ “pz 18 [LVAG-MAd-SHN VU 10 ALOANIWAAd-SLVAd U! patyoeds aq kewl OAL « +‘uoTe1odo [SVx JO} Aj[eusoyUT JoyndUIOd 11g 9] 98108 ZHI 8g ‘poeds-ysry Bann soz] e + +"9U0} DUAS 0006 UUL] Jo wNIqUUr] prepue}s 0} OUAS [ITAA © + +“ONYBA 9}OU poloapes Aue Je sas—nd jndyno 07 pewureigold 3q ACW SL Ad LNO YADONAL OML + +"ALVOOT 10 GOLS/AV 1d ‘LWddad “ASV + +SUIpNpoUr ‘suOTIOUN] posn A[UOUILUOS 94] JO AUBUT [O1]UOD AJ9]OWIAI 0} PousIsse oq ACUI ST AdNI HOLIMSLOO OME « +“SUIPIONAI I[IYM P2sesd JOU Iv $3}OU BUTISIXO—ZUIPIOIA SATON.ASOP-UON + +‘suoneurldxa peuoyippe sdeydsip uowng g1TqH + +oy] ‘pepsau JI ‘suoneiodo [ye yYsnosy] NOA sapins ApIespo Avfdsip QO] Joey Z7¢ 9y3—uoeIodo Urea] 0} Ased ‘aus « + +jUorel]suowtap & IO} Aepol Jayeap uur’] INOA dag ‘dISHUL +INOA 0} UONUS}]¥ PaplAIPUN INOA SUTJOASp ITY ps pue +p1osai ‘asoduod no Jay 0} pausisap st 1s0uenbesuur’] oy) +Aum Aposiooid $,Jeu], ‘SS9d0Id SATTBS1D OY} YIM SOIOJIOIUT +yey) xo]dwWI0d Os dq JOA9U P[NoUsS osn NOA AZopOuYdE} oy + +ISTUMOIAUIO?) NOAA UOHISOdWIO) + +"NOSpr] B Oy ‘AONUTJUT yada 0} seq Maz Se] BY] Jas UdAd +uvd NOA ‘palisap JJ ‘souanbes Mou ¥B OVUT sjied ou] [Te Adoo +ATesrewO Ne WI) [IM ONOS ALVAAO JeyIe80} wey} +,deyd,, 0} UOTOUNJ ONOS ALVA ou] asn usy] ‘saouanbes +JENPIAIpUt UI (“949 ‘snJOYD ‘aS1OA) UOTIDIS JIseq Yes +Pl0da1 OF ST ABM JOuIOUY “(812g 666 01 dn) ysnory) ABM + +dU} [fe YORI] YORs p10991 0} ST SUOS B 9789I9 0} ABM SUG, + +SUOS & SUTVAID + +*suoT}oes poJUBMUN + +SAOUIOI 0} ABM SWS dU} SoyeIodo SUV ALATAaG + +“OBPLIq dy} PUB SNIOY PUOdAS dT]] Ud9MIAQ SIDA ISI + +ay) Jo Adoo B JJasuT WYSE NOAA ‘afdwexs 10.f ‘UO JUSIN]JIP + +B IO aouaNbas sues OY} UI—JOY OUP 0} UOTIEIO] 9UO WOT] +$1Bq JAOUI OF NOA sMOTIe WOTIOUNS AdOO/IMASNI OULL + +‘SUIPIONAI JIVIS Udy) “OQuINU eq porisop ay} puy + +0} CNIMAY 10 ‘CYVM Od LSWA “AEVOOT esn Apduns + +sainjeay [PUOHIPPY + +‘gouanbas & UTYIIM s]UTOd a1y1dads 3¥ $9100 QnPIOAO OL "UOT} +-ouns dALLS ATONIS 24) Suisn pasueyo Jo ‘pasesa ‘pappe +aq osye ABUT S9]ON ‘U0 9q ]IIM 1 “yoeq podeyd uayM +—aouanbas oy] ul skeyd 71 a10J9q Isnf posers oq 0} d]0U ayy +ssaid pue ASvwug ploy Aydunis ‘jou Suomm & aseso OL + +sunipa + +jsesdueyo ureisoid pue ‘fepod ureysns +‘yonoplalje ‘AWOOTOA ‘UOTyeTNpow ‘pusg youd Surpnyour +pep10del are $199JJ2 TCTIN [WV iPeqqnpseao aq Aeur syoen +Ze 07 dn ‘Kem sie Uy *(foeI} JOyOUR OJOS 10 ALLAN +NOA ssofum) duAS yOaysod ul Avy [[IM Yow] ISI 93 “prooar +NOA 3[IYM—SUIPIOIA LIBIS PU YORI) TUdIOTJIP B JOaTas +*y1ed MOU B QNPIsA0 OL, “SuIps0daJ-jods 10} aouanbes mno0k +UI UOHBIO] Aue ssad0e ATYOIND 0} owt} Aue ye pasn aq AvUE +SJONUOD FLIVOOT pur ‘ANIMA ‘CYVMaYOd LSVd +{SUIPIOSAI {IY posesa JOU se So]OU SuTsTXO— +yous} 3U} OUT poppe aq JIM poteyd sajou yeuonippe Auy + +*(povesjap 10 poysn{pe oq ABW UOTIIII0D BUTUTT]) j{paqoeLI09 + +2q ][IM S1OLIe Sur [fe ATUO—patey]d nod Jey Jedy ]],NOA + +‘] req 0] punose yoeq sdoo] sduanbas ay] Udy AA “YOu Yor + +§,sa0uaNbas at} O] SUIT) UI preogday [IW] INO Avy usy3 +AV'1d pue (YOON ssoid Ayduus ‘aousnbes & p1o09es OF, + +g0uaNbas & SUIP10I0y] + +‘JONWOD s}JouNaI TeuONdGO e + +"UOTJEZIUOIYUAS OPOS UIT} FLAWS [euondo e + +‘sou .sulddoys, noyyM sayelodo pue yoegdvyd ZuLINp S¥IOM NOLLOANNYOO ONIWILL e + +‘onqea ory AY + +pojoojes-oid & ye sajou pyoy Aue syeadas ATTeONewWO Ne UOTOUNS [WAdAY OAISNOX e +‘LSVJ SUnIpS soyeu UOTOUN ASV UA OUlN-[eal SAISNIOXY e +‘Koy B JO YONO} 941 12 CASOdSNVALL 0g ABU Syde] [Te 10 9UC e + +i ASIP Jed + +S9}0U OOO‘OTT JOA SpfOy puv SpUOdeS UT SBUOS Xa[AUIOD So10}S DALIP YSIP , 74 € ISCJ-CNIN + +jSIOZISOUJUAS + +stuoydAjod of 0} dn skeyd A[snoourynuls ‘spouueYd [IW 9T JO duo 0} pousisse oq +ABUL YORI] YOR ‘syous) oruoydAjod ‘snoouelnurs 7¢ SuTeJUOS ssouUaNbas QO] OY} JO YORA e + +‘SJONUOS ATWOOT pur ‘GNIMAY ‘GaVM OA +LSVd ‘GYOOde AOLS ‘AV Td YIM Jopsocas ade} Yowsj-N[NU O} eps st UOTLISdO @ +LOPNOUT SaINjeoy s[quyIeUlss AUB S.JJ ‘OSN pue UIes] 0} o[duns A[suIzeUe JOA ‘PnJsomod APOUIOITXO +St 1] “UeIOIsNUL feUOIssajoid oY} 10 JOO} soUBULIOJIJAd pue UOTIsOduIOS 11e-dY1-JO-9}e)s B SI IONUANbDaguUT] ay + +JOps1odady soUINbIS [GTI YVAL ZE +Jgouanbaguury oy + \ No newline at end of file diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin deleted file mode 100644 index e308fb4c..00000000 Binary files a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin similarity index 51% rename from tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin rename to tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin index ea30bf21..518ac636 100644 --- a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin +++ b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin @@ -4,1059 +4,1059 @@ - - + + - - -
+ + +

- - The - LinnSequencer + + The + LinnSequencer - - 32 - Track - MIDI - Sequence - Recorder + + 32 + Track + MIDI + Sequence + Recorder

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is + + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is

- - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It’s - many - remarkable - features - include: + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It’s + many + remarkable + features + include:

- - ¢ - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST + + ¢ + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST - - FORWARD, - REWIND, - and - LOCATE - controls. + + FORWARD, + REWIND, + and + LOCATE + controls.

- - e - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may + + e + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic

- synthesizers! + synthesizers!

- - ¢ - Ultra-fast - 3%” - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes + + ¢ + Ultra-fast + 3%” + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes

- per - disk! + per + disk!

- - ¢ - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. + + ¢ + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. - - e - Exclusive - real-time - ERASE - function - makes - editing - FAST. + + e + Exclusive + real-time + ERASE + function + makes + editing + FAST. - - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected + + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected

- rhythmic - value. + rhythmic + value.

- - ¢ - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes. + + ¢ + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes.

- - ¢ - Optional - SMPTE - time - code - synchronization. + + ¢ + Optional + SMPTE + time + code + synchronization.

- © - Optional - remote - control. + © + Optional + remote + control.

- Recording - a - Sequence + Recording + a + Sequence

- To - record - a - sequence, - simply - press - RECORD - and - PLAY, + To + record + a + sequence, + simply + press + RECORD + and + PLAY, - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s - click - track. - When - the - sequence - loops - back - around - to - bar - 1, + click + track. + When + the + sequence + loops + back + around + to + bar + 1, - you’ - ll - hear - what - you - played—only - all - timing - errors - will - be + you’ + ll + hear + what + you + played—only + all + timing + errors + will + be

- - corrected! - (Timing - correction - may - be - adjusted - or - defeated). + + corrected! + (Timing + correction + may + be + adjusted + or + defeated).

- Any - additional - notes - played - will - be - added - into - the - track + Any + additional + notes + played + will + be + added + into + the + track - — - existing - notes - are - not - erased - while - recording! + — + existing + notes + are + not + erased + while + recording!

- FAST - FORWARD, - REWIND, - and - LOCATE - controls + FAST + FORWARD, + REWIND, + and + LOCATE + controls - may - be - used - at - any - time - to - quickly - access - any - location - in + may + be + used + at + any + time + to + quickly + access + any + location + in - your - sequence - for - spot-recording. - To - overdub - a - new - part, + your + sequence + for + spot-recording. + To + overdub + a + new + part, - select - a - different - track - and - start - recording—while - you + select + a + different + track + and + start + recording—while + you - record, - the - first - track - will - play - in - perfect - sync - (unless - you + record, + the + first + track + will + play + in + perfect + sync + (unless + you - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded - including - pitch - bend, - modulation, - velocity, - aftertouch, + including + pitch + bend, + modulation, + velocity, + aftertouch, - sustain - pedal, - and - program - changes! + sustain + pedal, + and + program + changes!

- Editing + Editing

- To - erase - a - wrong - note, - simply - hold - ERASE - and - press + To + erase + a + wrong + note, + simply + hold + ERASE + and + press - the - note - to - be - erased - just - before - it - plays - in - the - sequence— + the + note + to + be + erased + just + before + it + plays + in + the + sequence— - when - played - back, - it - will - be - gone. - Notes - may - also - be + when + played + back, + it + will + be + gone. + Notes + may + also + be

- added, - erased, - or - changed - using - the - SINGLE - STEP - func- + added, + erased, + or + changed + using + the + SINGLE + STEP + func- - tion. - To - overdub - notes - at - specific - points - within - a - sequence, + tion. + To + overdub + notes + at + specific + points + within + a + sequence,

- Additional - Features + Additional + Features

- simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to - find - the - desired - bar - number, - then - start - recording. + find + the + desired + bar + number, + then + start + recording.

- The - INSERT/COPY - function - allows - you - to - move - bars + The + INSERT/COPY + function + allows + you + to + move + bars - from - one - location - to - another—in - the - same - sequence - or - a + from + one + location + to + another—in + the + same + sequence + or + a - different - one. - For - example, - you - might - insert - a - copy - of - the + different + one. + For + example, + you + might + insert + a + copy + of + the - first - verse - between - the - second - chorus - and - the - bridge. + first + verse + between + the + second + chorus + and + the + bridge. - DELETE - BARS - operates - the - same - way - to - remove + DELETE + BARS + operates + the + same + way + to + remove - unwanted - sections, + unwanted + sections,

- Creating - a - Song + Creating + a + Song

- One - way - to - create - a - song - is - to - record - each - track - all - the + One + way + to + create + a + song + is + to + record + each + track + all + the - way - through - (up - to - 999 - bars). - Another - way - is - to - record + way + through + (up + to + 999 + bars). + Another + way + is + to + record - each - basic - section - (verse, - chorus, - etc.) - in - individual + each + basic + section + (verse, + chorus, + etc.) + in + individual - sequences, - then - use - the - CREATE - SONG - function - to - “chain” + sequences, + then + use + the + CREATE + SONG + function + to + “chain” - them - together. - CREATE - SONG - will - then - automatically + them + together. + CREATE + SONG + will + then + automatically - copy - all - the - parts - into - a - new - sequence. - If - desired, - you - can + copy + all + the + parts + into + a + new + sequence. + If + desired, + you + can - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout.

- Composition - Without - Compromise + Composition + Without + Compromise

- The - technology - you - use - should - never - be - so - complex - that + The + technology + you + use + should + never + be + so + complex + that - it - interferes - with - the - creative - process. - That’s - precisely - why + it + interferes + with + the + creative + process. + That’s + precisely + why - the - LinnSequencer - is - designed - to - let - you - compose, - record + the + LinnSequencer + is + designed + to + let + you + compose, + record - and - edit - while - devoting - your - undivided - attention - to - your + and + edit + while + devoting + your + undivided + attention + to + your - music. - See - your - Linn - dealer - today - for - a - demonstration! + music. + See + your + Linn + dealer + today + for + a + demonstration!

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations. - If - needed, - the + + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations. + If + needed, + the

- HELP - button - displays - additional - explanations. + HELP + button + displays + additional + explanations.

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. + + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. - - ¢ - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including + + ¢ + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including

- ERASE, - REPEAT, - PLAY/STOP, - or - LOCATE. + ERASE, + REPEAT, + PLAY/STOP, + or + LOCATE.

- - ¢ - Iwo - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. + + ¢ + Iwo + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value.

- © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone. + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone.

- - © - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. + + © + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. - - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, + + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second,

- (even - drop - frame!) + (even + drop + frame!)

- - ¢ - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes + + ¢ + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes

- on - the - TAP - TEMPO - button. + on + the + TAP + TEMPO + button.

- - ¢ - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. + + ¢ + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. - - ¢ - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. + + ¢ + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song.

- linn + linn - Linn - Electronics, - Inc. + 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

diff --git a/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/txt.bin rename to tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..aa26441b Binary files /dev/null and b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/txt.bin b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..137fef56 --- /dev/null +++ b/tests/cache/cardinal/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,124 @@ +2A NNI‘I 6F6867# XATALL IE18-80L (818) + +9SEI6 VO “BUBZIRY, “J0aNS PIPUXO OZLEI +“Uy ‘soTUOMOI,q UUrT + +uu + +“‘SUOS B UIJIM pasueyo oq ABU pue ‘posn oq AWW AYN IVNOIS AWLL AUV +“parlsop Jr SUOTIISUBI} YIOOUIS YIM “BoueNbas eB OJUI pourtueIZOId 9q ABU SFONWHO OdINAL e + +‘uonng OdNAL dV L 9) uO + +sojou Jayienb Suiddy} Aq 10 ‘syUSTIOIOUI oINUTIAI-J8g-Jesg & JO sys} UL ofquisn(pe ‘ATTeouIAUINU paiajus oq ABU OdINALL e + +(jouer doup u3a9) + +“puooes Jed souely O€ 10 “SZ “pz 18 [LVAG-MAd-SHN VU 10 ALOANIWAAd-SLVAd U! patyoeds aq kewl OAL « +‘uoTe1odo [SVx JO} Aj[eusoyUT JoyndUIOd 11g 9] 98108 ZHI 8g ‘poeds-ysry Bann soz] e + +"9U0} DUAS 0006 UUL] Jo wNIqUUr] prepue}s 0} OUAS [ITAA © + +“ONYBA 9}OU poloapes Aue Je sas—nd jndyno 07 pewureigold 3q ACW SL Ad LNO YADONAL OML + +"ALVOOT 10 GOLS/AV 1d ‘LWddad “ASV + +SUIpNpoUr ‘suOTIOUN] posn A[UOUILUOS 94] JO AUBUT [O1]UOD AJ9]OWIAI 0} PousIsse oq ACUI ST AdNI HOLIMSLOO OME « +“SUIPIONAI I[IYM P2sesd JOU Iv $3}OU BUTISIXO—ZUIPIOIA SATON.ASOP-UON + +‘suoneurldxa peuoyippe sdeydsip uowng g1TqH + +oy] ‘pepsau JI ‘suoneiodo [ye yYsnosy] NOA sapins ApIespo Avfdsip QO] Joey Z7¢ 9y3—uoeIodo Urea] 0} Ased ‘aus « + +jUorel]suowtap & IO} Aepol Jayeap uur’] INOA dag ‘dISHUL +INOA 0} UONUS}]¥ PaplAIPUN INOA SUTJOASp ITY ps pue +p1osai ‘asoduod no Jay 0} pausisap st 1s0uenbesuur’] oy) +Aum Aposiooid $,Jeu], ‘SS9d0Id SATTBS1D OY} YIM SOIOJIOIUT +yey) xo]dwWI0d Os dq JOA9U P[NoUsS osn NOA AZopOuYdE} oy + +ISTUMOIAUIO?) NOAA UOHISOdWIO) + +"NOSpr] B Oy ‘AONUTJUT yada 0} seq Maz Se] BY] Jas UdAd +uvd NOA ‘palisap JJ ‘souanbes Mou ¥B OVUT sjied ou] [Te Adoo +ATesrewO Ne WI) [IM ONOS ALVAAO JeyIe80} wey} +,deyd,, 0} UOTOUNJ ONOS ALVA ou] asn usy] ‘saouanbes +JENPIAIpUt UI (“949 ‘snJOYD ‘aS1OA) UOTIDIS JIseq Yes +Pl0da1 OF ST ABM JOuIOUY “(812g 666 01 dn) ysnory) ABM + +dU} [fe YORI] YORs p10991 0} ST SUOS B 9789I9 0} ABM SUG, + +SUOS & SUTVAID + +*suoT}oes poJUBMUN + +SAOUIOI 0} ABM SWS dU} SoyeIodo SUV ALATAaG + +“OBPLIq dy} PUB SNIOY PUOdAS dT]] Ud9MIAQ SIDA ISI + +ay) Jo Adoo B JJasuT WYSE NOAA ‘afdwexs 10.f ‘UO JUSIN]JIP + +B IO aouaNbas sues OY} UI—JOY OUP 0} UOTIEIO] 9UO WOT] +$1Bq JAOUI OF NOA sMOTIe WOTIOUNS AdOO/IMASNI OULL + +‘SUIPIONAI JIVIS Udy) “OQuINU eq porisop ay} puy + +0} CNIMAY 10 ‘CYVM Od LSWA “AEVOOT esn Apduns + +sainjeay [PUOHIPPY + +‘gouanbas & UTYIIM s]UTOd a1y1dads 3¥ $9100 QnPIOAO OL "UOT} +-ouns dALLS ATONIS 24) Suisn pasueyo Jo ‘pasesa ‘pappe +aq osye ABUT S9]ON ‘U0 9q ]IIM 1 “yoeq podeyd uayM +—aouanbas oy] ul skeyd 71 a10J9q Isnf posers oq 0} d]0U ayy +ssaid pue ASvwug ploy Aydunis ‘jou Suomm & aseso OL + +sunipa + +jsesdueyo ureisoid pue ‘fepod ureysns +‘yonoplalje ‘AWOOTOA ‘UOTyeTNpow ‘pusg youd Surpnyour +pep10del are $199JJ2 TCTIN [WV iPeqqnpseao aq Aeur syoen +Ze 07 dn ‘Kem sie Uy *(foeI} JOyOUR OJOS 10 ALLAN +NOA ssofum) duAS yOaysod ul Avy [[IM Yow] ISI 93 “prooar +NOA 3[IYM—SUIPIOIA LIBIS PU YORI) TUdIOTJIP B JOaTas +*y1ed MOU B QNPIsA0 OL, “SuIps0daJ-jods 10} aouanbes mno0k +UI UOHBIO] Aue ssad0e ATYOIND 0} owt} Aue ye pasn aq AvUE +SJONUOD FLIVOOT pur ‘ANIMA ‘CYVMaYOd LSVd +{SUIPIOSAI {IY posesa JOU se So]OU SuTsTXO— +yous} 3U} OUT poppe aq JIM poteyd sajou yeuonippe Auy +*(povesjap 10 poysn{pe oq ABW UOTIIII0D BUTUTT]) j{paqoeLI09 +2q ][IM S1OLIe Sur [fe ATUO—patey]d nod Jey Jedy ]],NOA +‘] req 0] punose yoeq sdoo] sduanbas ay] Udy AA “YOu Yor +§,sa0uaNbas at} O] SUIT) UI preogday [IW] INO Avy usy3 +AV'1d pue (YOON ssoid Ayduus ‘aousnbes & p1o09es OF, + +g0uaNbas & SUIP10I0y] + +‘JONWOD s}JouNaI TeuONdGO e + +"UOTJEZIUOIYUAS OPOS UIT} FLAWS [euondo e + +‘sou .sulddoys, noyyM sayelodo pue yoegdvyd ZuLINp S¥IOM NOLLOANNYOO ONIWILL e + +‘onqea ory AY + +pojoojes-oid & ye sajou pyoy Aue syeadas ATTeONewWO Ne UOTOUNS [WAdAY OAISNOX e +‘LSVJ SUnIpS soyeu UOTOUN ASV UA OUlN-[eal SAISNIOXY e +‘Koy B JO YONO} 941 12 CASOdSNVALL 0g ABU Syde] [Te 10 9UC e + +i ASIP Jed + +S9}0U OOO‘OTT JOA SpfOy puv SpUOdeS UT SBUOS Xa[AUIOD So10}S DALIP YSIP , 74 € ISCJ-CNIN + +jSIOZISOUJUAS + +stuoydAjod of 0} dn skeyd A[snoourynuls ‘spouueYd [IW 9T JO duo 0} pousisse oq +ABUL YORI] YOR ‘syous) oruoydAjod ‘snoouelnurs 7¢ SuTeJUOS ssouUaNbas QO] OY} JO YORA e + +‘SJONUOS ATWOOT pur ‘GNIMAY ‘GaVM OA +LSVd ‘GYOOde AOLS ‘AV Td YIM Jopsocas ade} Yowsj-N[NU O} eps st UOTLISdO @ +LOPNOUT SaINjeoy s[quyIeUlss AUB S.JJ ‘OSN pue UIes] 0} o[duns A[suIzeUe JOA ‘PnJsomod APOUIOITXO +St 1] “UeIOIsNUL feUOIssajoid oY} 10 JOO} soUBULIOJIJAd pue UOTIsOduIOS 11e-dY1-JO-9}e)s B SI IONUANbDaguUT] ay + +JOps1odady soUINbIS [GTI YVAL ZE +Jgouanbaguury oy + \ No newline at end of file diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin deleted file mode 100644 index 2d1bd1c9..00000000 --- a/tests/cache/cardinal/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Warning. Invalid resolution 0 dpi. Using 70 instead. diff --git a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stderr.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stderr.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stdout.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout/stderr.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout/stderr.bin deleted file mode 100644 index 2d1bd1c9..00000000 --- a/tests/cache/cardinal/__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Warning. Invalid resolution 0 dpi. Using 70 instead. diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout/stderr.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout/stderr.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout/stdout.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout/stderr.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout/stderr.bin deleted file mode 100644 index 2d1bd1c9..00000000 --- a/tests/cache/cardinal/__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Warning. Invalid resolution 0 dpi. Using 70 instead. diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout/stderr.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout/stderr.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout/stdout.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout/stderr.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout/stderr.bin deleted file mode 100644 index 2d1bd1c9..00000000 --- a/tests/cache/cardinal/__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Warning. Invalid resolution 0 dpi. Using 70 instead. diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout/stderr.bin similarity index 100% rename from tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout/stderr.bin diff --git a/tests/cache/cardinal/__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout/stdout.bin b/tests/cache/cardinal/__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout/stdout.bin rename to tests/cache/cardinal/__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout/stdout.bin diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index 089cb041..00000000 Binary files a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 51% rename from tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index b76beb1d..d06920de 100644 --- a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,1059 +4,1059 @@ - - + + - - -
+ + +

- - The - LinnSequencer + + The + LinnSequencer - - 32 - Track - MIDI - Sequence - Recorder + + 32 + Track + MIDI + Sequence + Recorder

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is + + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is

- - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It’s - many - remarkable - features - include: + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It’s + many + remarkable + features + include:

- - ¢ - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST + + ¢ + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST - - FORWARD, - REWIND, - and - LOCATE - controls. + + FORWARD, + REWIND, + and + LOCATE + controls.

- - e - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may + + e + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic

- synthesizers! + synthesizers!

- - ¢ - Ultra-fast - 3%” - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes + + ¢ + Ultra-fast + 3%” + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes

- per - disk! + per + disk!

- - ¢ - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. + + ¢ + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. - - e - Exclusive - real-time - ERASE - function - makes - editing - FAST. + + e + Exclusive + real-time + ERASE + function + makes + editing + FAST. - - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected + + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected

- rhythmic - value. + rhythmic + value.

- - ¢ - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes. + + ¢ + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes.

- - ¢ - Optional - SMPTE - time - code - synchronization. + + ¢ + Optional + SMPTE + time + code + synchronization.

- © - Optional - remote - control. + © + Optional + remote + control.

- Recording - a - Sequence + Recording + a + Sequence

- To - record - a - sequence, - simply - press - RECORD - and - PLAY, + To + record + a + sequence, + simply + press + RECORD + and + PLAY, - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s - click - track. - When - the - sequence - loops - back - around - to - bar - 1, + click + track. + When + the + sequence + loops + back + around + to + bar + 1, - you’ - ll - hear - what - you - played—only - all - timing - errors - will - be + you’ + ll + hear + what + you + played—only + all + timing + errors + will + be

- - corrected! - (Timing - correction - may - be - adjusted - or - defeated). + + corrected! + (Timing + correction + may + be + adjusted + or + defeated).

- Any - additional - notes - played - will - be - added - into - the - track + Any + additional + notes + played + will + be + added + into + the + track - — - existing - notes - are - not - erased - while - recording! + — + existing + notes + are + not + erased + while + recording!

- FAST - FORWARD, - REWIND, - and - LOCATE - controls + FAST + FORWARD, + REWIND, + and + LOCATE + controls - may - be - used - at - any - time - to - quickly - access - any - location - in + may + be + used + at + any + time + to + quickly + access + any + location + in - your - sequence - for - spot-recording. - To - overdub - a - new - part, + your + sequence + for + spot-recording. + To + overdub + a + new + part, - select - a - different - track - and - start - recording—while - you + select + a + different + track + and + start + recording—while + you - record, - the - first - track - will - play - in - perfect - sync - (unless - you + record, + the + first + track + will + play + in + perfect + sync + (unless + you - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded - including - pitch - bend, - modulation, - velocity, - aftertouch, + including + pitch + bend, + modulation, + velocity, + aftertouch, - sustain - pedal, - and - program - changes! + sustain + pedal, + and + program + changes!

- Editing + Editing

- To - erase - a - wrong - note, - simply - hold - ERASE - and - press + To + erase + a + wrong + note, + simply + hold + ERASE + and + press - the - note - to - be - erased - just - before - it - plays - in - the - sequence— + the + note + to + be + erased + just + before + it + plays + in + the + sequence— - when - played - back, - it - will - be - gone. - Notes - may - also - be + when + played + back, + it + will + be + gone. + Notes + may + also + be

- added, - erased, - or - changed - using - the - SINGLE - STEP - func- + added, + erased, + or + changed + using + the + SINGLE + STEP + func- - tion. - To - overdub - notes - at - specific - points - within - a - sequence, + tion. + To + overdub + notes + at + specific + points + within + a + sequence,

- Additional - Features + Additional + Features

- simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to - find - the - desired - bar - number, - then - start - recording. + find + the + desired + bar + number, + then + start + recording.

- The - INSERT/COPY - function - allows - you - to - move - bars + The + INSERT/COPY + function + allows + you + to + move + bars - from - one - location - to - another—in - the - same - sequence - or - a + from + one + location + to + another—in + the + same + sequence + or + a - different - one. - For - example, - you - might - insert - a - copy - of - the + different + one. + For + example, + you + might + insert + a + copy + of + the - first - verse - between - the - second - chorus - and - the - bridge. + first + verse + between + the + second + chorus + and + the + bridge. - DELETE - BARS - operates - the - same - way - to - remove + DELETE + BARS + operates + the + same + way + to + remove - unwanted - sections, + unwanted + sections,

- Creating - a - Song + Creating + a + Song

- One - way - to - create - a - song - is - to - record - each - track - all - the + One + way + to + create + a + song + is + to + record + each + track + all + the - way - through - (up - to - 999 - bars). - Another - way - is - to - record + way + through + (up + to + 999 + bars). + Another + way + is + to + record - each - basic - section - (verse, - chorus, - etc.) - in - individual + each + basic + section + (verse, + chorus, + etc.) + in + individual - sequences, - then - use - the - CREATE - SONG - function - to - “chain” + sequences, + then + use + the + CREATE + SONG + function + to + “chain” - them - together. - CREATE - SONG - will - then - automatically + them + together. + CREATE + SONG + will + then + automatically - copy - all - the - parts - into - a - new - sequence. - If - desired, - you - can + copy + all + the + parts + into + a + new + sequence. + If + desired, + you + can - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout.

- Composition - Without - Compromise + Composition + Without + Compromise

- The - technology - you - use - should - never - be - so - complex - that + The + technology + you + use + should + never + be + so + complex + that - it - interferes - with - the - creative - process. - That’s - precisely - why + it + interferes + with + the + creative + process. + That’s + precisely + why - the - LinnSequencer - is - designed - to - let - you - compose, - record + the + LinnSequencer + is + designed + to + let + you + compose, + record - and - edit - while - devoting - your - undivided - attention - to - your + and + edit + while + devoting + your + undivided + attention + to + your - music. - See - your - Linn - dealer - today - for - a - demonstration! + music. + See + your + Linn + dealer + today + for + a + demonstration!

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations. - If - needed, - the + + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations. + If + needed, + the

- HELP - button - displays - additional - explanations. + HELP + button + displays + additional + explanations.

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. + + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. - - ¢ - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including + + ¢ + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including

- ERASE, - REPEAT, - PLAY/STOP, - or - LOCATE. + ERASE, + REPEAT, + PLAY/STOP, + or + LOCATE.

- - ¢ - Iwo - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. + + ¢ + Iwo + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value.

- © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone. + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone.

- - © - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. + + © + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. - - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, + + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second,

- (even - drop - frame!) + (even + drop + frame!)

- - ¢ - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes + + ¢ + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes

- on - the - TAP - TEMPO - button. + on + the + TAP + TEMPO + button.

- - ¢ - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. + + ¢ + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. - - ¢ - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. + + ¢ + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song.

- linn + linn - Linn - Electronics, - Inc. + 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

diff --git a/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/txt.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/txt.bin rename to tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..88bb9fc9 Binary files /dev/null and b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/txt.bin b/tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/txt.bin rename to tests/cache/ccitt/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin deleted file mode 100644 index 07716963..00000000 --- a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin +++ /dev/null @@ -1,9 +0,0 @@ -Multicolor Black -Pure Black (K = 100} - -Pure Magenta - -Pure Cyan - -Pure Yellow - \ No newline at end of file diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin deleted file mode 100644 index 698f2e8a..00000000 --- a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - -
-
-

- - Multicolor - Black - - - Pure - Black - (K - = - 100} - -

-
-
-

- - Pure - Magenta - -

-
-
-

- - Pure - Cyan - -

-
-
-

- - Pure - Yellow - -

-
-
- - diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin deleted file mode 100644 index 07716963..00000000 --- a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin +++ /dev/null @@ -1,9 +0,0 @@ -Multicolor Black -Pure Black (K = 100} - -Pure Magenta - -Pure Cyan - -Pure Yellow - \ No newline at end of file diff --git a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/txt.bin deleted file mode 100644 index 080288f6..00000000 --- a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/txt.bin +++ /dev/null @@ -1,13 +0,0 @@ -Portez ce vieux whisky au juge -blond qui fume sur son ile -interieure, a cöte de l'alcöve -ovolde, oU les büches se -consument dans l'ätre, ce qui -lui permet de penser ä la -cgnogenese de l'&tre dont il -est question dans la cause -ambigu6 entendue ä Moy, dans -un capharnaüÜm qui, pense-t-il, -diminue ca et 13 la qualite de son -ceuvre. - \ No newline at end of file diff --git a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 72% rename from tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index 40830d1a..750ad232 100644 Binary files a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin new file mode 100644 index 00000000..25fdded2 --- /dev/null +++ b/tests/cache/francais/__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin @@ -0,0 +1,13 @@ +Portez ce vieux whisky au juge +blond qui fume sur son Ile +interieure, a cöte de l'alcöve +ovoide, oU les büches se +consument dans l'ätre, ce qui +lui permet de penser & la +caenogenese de |'etre dont il +est question dans la cause +ambigu& entendue a MoY, dans +un capharnaüm qui, pense-t-il, +diminue ca et la la qualite de son +ceuvre. + \ No newline at end of file diff --git a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/txt.bin deleted file mode 100644 index 4ded2d60..00000000 --- a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/txt.bin +++ /dev/null @@ -1,13 +0,0 @@ -Portez ce vieux whisky au juge -blond qui fume sur son île -intérieure, à côté de l'alcôve -ovoide, où les bûches se -consument dans l'âtre, ce qui -lui permet de penser à la -cænogénèse de l'être dont il -est question dans la cause -ambiguë entendue à Moÿ, dans -un capharnaüm qui, pense-t-il, -diminue cà et là la qualité de son -œuvre. - \ No newline at end of file diff --git a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 78% rename from tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index e0cb3317..74e98d6d 100644 Binary files a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/graph_ocred/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/graph_ocred/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index d89942da..00000000 Binary files a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 57% rename from tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index c9ae9039..946585c5 100644 --- a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,472 +4,472 @@ - - + + - - -
-
-

- - 4ist - ConGREss, - } - SENATE. - { - Ex. - Doc, + + +

+
+

+ + 4ist + ConGREss, + } + SENATE. + { + Ex. + Doc, - 3d - Session. - No. - 25. + 3d + Session. + No. + 25.

- MESSAGE + MESSAGE

- OF - THE + OF + THE

- PRESIDENT - OF - THE - UNITED - STATES, + PRESIDENT + OF + THE + UNITED + STATES,

- COMMUNICATING + COMMUNICATING

- A - copy - of - regulations - for - the - consular - courts - of - the - United - States - in - Japan, + A + copy + of + regulations + for + the + consular + courts + of + the + United + States + in + Japan, - decreed - and - issued - by - the - minister - of - the - United - States - in - that - country. + decreed + and + issued + by + the + minister + of + the + United + States + in + that + country.

-
-

- - JANUARY - 27, - 1871,—Read, - referred - to - the - Committee - on - Commerce, - and - ordered - to - be +

+

+ + JANUARY + 27, + 1871,—Read, + referred + to + the + Committee + on + Commerce, + and + ordered + to + be - - printed. + + printed.

- To - the - Senate - and - House - of - Representatives - : + To + the + Senate + and + House + of + Representatives + :

- I - transmit - herewith, - for - the - consideration - of - Congress, - a - report - from + I + transmit + herewith, + for + the + consideration + of + Congress, + a + report + from - the - Secretary - of - State, - and - the - papers - which - accompanied - it, - concern- + the + Secretary + of + State, + and + the + papers + which + accompanied + it, + concern- - ing - regulations - for - the - consular - courts - of - the - United - States - in - Japan. + ing + regulations + for + the + consular + courts + of + the + United + States + in + Japan.

- U. - 8. - GRANT. + U. + 8. + GRANT.

- ‘WASHINGTON, - January - 27, - 1871. + ‘WASHINGTON, + January + 27, + 1871.

- DEPARTMENT - OF - STATE, + DEPARTMENT + OF + STATE, - Washington, - January - 26, - 1870, + Washington, + January + 26, + 1870,

- The - Secretary - of - State - has - the - honor - to - submit - herewith, - for - revision + The + Secretary + of + State + has + the + honor + to + submit + herewith, + for + revision - by - Congress, - in - conformity - with - the - provisions - of - section - 6 - of - the - act + by + Congress, + in + conformity + with + the + provisions + of + section + 6 + of + the + act - approved - 22d - of - June, - 1860, - a - copy - of - “regulations - for - the - consular + approved + 22d + of + June, + 1860, + a + copy + of + “regulations + for + the + consular - courts - of - the - United - States - in - Japan,” - decreed - and - issued - by - C. - BE. + courts + of + the + United + States + in + Japan,” + decreed + and + issued + by + C. + BE. - De - Long, - the - minister - of - the - United - States - in - that - country, - in - Septem- + De + Long, + the + minister + of + the + United + States + in + that + country, + in + Septem- - ber, - 1870; - and - also - the - papers - mentioned - in - the - subjoined - list, - which, + ber, + 1870; + and + also + the + papers + mentioned + in + the + subjoined + list, + which, - contain - suggestions - on - the - subject - thereof. + contain + suggestions + on + the + subject + thereof.

- A - copy - of - Article - XXVI - of - the - consular - regulations - is - also - submitted, + A + copy + of + Article + XXVI + of + the + consular + regulations + is + also + submitted, - and - the - Secretary - of - State - respectfully - suggests, - for - the - consideration + and + the + Secretary + of + State + respectfully + suggests, + for + the + consideration - of - Congress, - the - propriety - of - limiting - the - power - of - ministers - to - make + of + Congress, + the + propriety + of + limiting + the + power + of + ministers + to + make - decrees - and - regulation, - in - the - sense - in - which - it - is - limited - by - paragraph + decrees + and + regulation, + in + the + sense + in + which + it + is + limited + by + paragraph - 431 - of - the - article - before - named—that - is, - “to - acts - necessary - to - organize + 431 + of + the + article + before + named—that + is, + “to + acts + necessary + to + organize - and - give - efficiency - to - the - courts - created - by - the - act.” + and + give + efficiency + to + the + courts + created + by + the + act.”

- Respectfully - submitted. + Respectfully + submitted.

- HAMILTON - FISH. + HAMILTON + FISH.

- The - PRESIDENT, + The + PRESIDENT,

- List - of - accompanying - papers. + List + of + accompanying + papers.

- 1, - Regulations - for - the - consular - courts - of - the - United - States - in - Japan. + 1, + Regulations + for + the + consular + courts + of + the + United + States + in + Japan. - 2, - Mr. - Fish - to - Mr. - De - Long, - September - 10, - 1870, + 2, + Mr. + Fish + to + Mr. + De + Long, + September + 10, + 1870,

- +

- +

- +

diff --git a/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/francais/__-l__fra__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/jbig2/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..96c1415a Binary files /dev/null and b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/francais/__-l__deu__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/jbig2/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/jbig2/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin deleted file mode 100644 index 1ffbc424..00000000 --- a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - -
-
-

- - - -

-
-
- - diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin new file mode 100644 index 00000000..fc7f0c29 --- /dev/null +++ b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -0,0 +1,22 @@ + + + + + + + + + + +
+
+

+ + + +

+
+
+ + diff --git a/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/cmyk/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 91% rename from tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index db319e97..2987783d 100644 Binary files a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/cmyk/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/lichtenstein/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/lichtenstein/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/manifest.jsonl b/tests/cache/manifest.jsonl index 6ec2a17e..f482aaaa 100644 --- a/tests/cache/manifest.jsonl +++ b/tests/cache/manifest.jsonl @@ -1,47 +1,44 @@ -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/cmyk.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/cmyk.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000004.ocr.png__000004__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000004.ocr.png", "$TMPDIR/000004", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000003.ocr.png__000003__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000003.ocr.png", "$TMPDIR/000003", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000005.ocr.png__000005__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000005.ocr.png", "$TMPDIR/000005", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000004.ocr.png__000004.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004.ocr.png", "$TMPDIR/000004.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000003.ocr.png__000003.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003.ocr.png", "$TMPDIR/000003.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000006.ocr.png__000006__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006.ocr.png", "$TMPDIR/000006", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000005.ocr.png__000005.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000005.ocr.png", "$TMPDIR/000005.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000006.ocr.png__000006__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006.ocr.png", "$TMPDIR/000006", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000006.ocr.png__000006.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006.ocr.png", "$TMPDIR/000006.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__fra__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "fra", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000002.ocr.png__000002.text__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002.ocr.png", "$TMPDIR/000002.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/2400dpi.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001.ocr.preview.jpg", "stdout"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__osd__--psm__0__000004.ocr.preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000004.ocr.preview.jpg", "stdout"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__osd__--psm__0__000003.ocr.preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000003.ocr.preview.jpg", "stdout"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__osd__--psm__0__000002.ocr.preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000002.ocr.preview.jpg", "stdout"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000002.ocr.png__000002.text__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002.ocr.png", "$TMPDIR/000002.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000003.ocr.png__000003.text__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003.ocr.png", "$TMPDIR/000003.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000004.ocr.png__000004.text__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004.ocr.png", "$TMPDIR/000004.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout", "sourcefile": "resources/poster.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001.ocr.preview.jpg", "stdout"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000001.ocr.png__000001__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000004.ocr.png__000004__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000004.ocr.png", "$TMPDIR/000004", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000003.ocr.png__000003__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000003.ocr.png", "$TMPDIR/000003", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.0.0-x86_64-i386-64bit", "python": "3.7.1", "argv_slug": "__-l__eng__000002.ocr.png__000002__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000002.ocr.png", "$TMPDIR/000002", "hocr", "txt"]} -{"tesseract_version": "tesseract 4.0.0 leptonica-1.77.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.36 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.0 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.2.0-x86_64-i386-64bit", "python": "3.7.2", "argv_slug": "__-l__deu__000001.ocr.png__000001.text__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001.ocr.png", "$TMPDIR/000001.text", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/2400dpi.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_hocr", "hocr", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_tess", "pdf", "txt"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/poster.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000002_rasterize_preview.jpg", "stdout"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000003_rasterize_preview.jpg", "stdout"]} +{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000004_rasterize_preview.jpg", "stdout"]} \ No newline at end of file diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index b34a5d2d..00000000 Binary files a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 1744d114..00000000 --- a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1,2 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica -Detected 60 diacritics diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 1744d114..00000000 --- a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1,2 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica -Detected 60 diacritics diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 55% rename from tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index 873adcaa..38166827 100644 --- a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,320 +4,320 @@ - - + + - - -
+ + +

- +

- - THEY - TIP-TOED - ALONG. + + THEY + TIP-TOED + ALONG.

-
-

- - ee - . +

+

+ + ee + . - - Se - We - went - tip-toeing - along - a - path - amongst + + Se + We + went + tip-toeing + along + a + path + amongst

- the - trees - back - towards - the - end - of - the + the + trees + back + towards + the + end + of + the - widow’s - garden, - stooping - down - so - as + widow’s + garden, + stooping + down + so + as - the - branches - wouldn’t - scrape - our - heads. + the + branches + wouldn’t + scrape + our + heads. - When - we - was - passing - by - the - kitchen + When + we + was + passing + by + the + kitchen - I - fell - over - a - root - and - made - a - noise. + I + fell + over + a + root + and + made + a + noise. - We - scrouched - down - and - laid - still. + We + scrouched + down + and + laid + still. - Miss - Watson’s - big - nigger, - named + Miss + Watson’s + big + nigger, + named - Jim, - was - setting - in - the - kitchen - door - ; + Jim, + was + setting + in + the + kitchen + door + ; - we - could - see - him - pretty - clear, - because + we + could + see + him + pretty + clear, + because - there - was - a - light - behind - him. - He + there + was + a + light + behind + him. + He - got - up - and - stretched - his - neck - out + got + up + and + stretched + his + neck + out - about - a - minute, - listening. - Then - he + about + a + minute, + listening. + Then + he - says, + says,

- “Who - dah?” + “Who + dah?”

- He - listened - some - more; - then - he + He + listened + some + more; + then + he - come - tip-toeing - down - and_ - stood + come + tip-toeing + down + and_ + stood - right - between - us; - we - could - a - touched + right + between + us; + we + could + a + touched - him, - nearly. - Well, - likely - it - was - min- + him, + nearly. + Well, + likely + it + was + min- - utes - and - minutes - that - there - warn’t - a + utes + and + minutes + that + there + warn’t + a - sound, - and - we - all - there - so - close + sound, + and + we + all + there + so + close - together. - There - was - a - place - on - my + together. + There + was + a + place + on + my - ankle - that - got - to - itching; - but - I + ankle + that + got + to + itching; + but + I

- - dasn’t - scratch - it; - and - then - my - ear - begun - to - itch; - and - next - my - back, - right - be- + + dasn’t + scratch + it; + and + then + my + ear + begun + to + itch; + and + next + my + back, + right + be- - - tween - my - shoulders. - Seemed - like - I’d - die - if - I - couldn’t - scratch. - Well, - I’ve + + tween + my + shoulders. + Seemed + like + I’d + die + if + I + couldn’t + scratch. + Well, + I’ve

- - noticed - that - thing - plenty - of - times - since. + + noticed + that + thing + plenty + of + times + since.

- Tf - you - are - with - the - quality, - or - at - a + Tf + you + are + with + the + quality, + or + at + a

- - funeral, - or - trying - to - go - to - sleep - when - you - ain’t - sleepy—if - you - are - anywheres + + funeral, + or + trying + to + go + to + sleep + when + you + ain’t + sleepy—if + you + are + anywheres

diff --git a/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..25cb8633 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1,2 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica +Detected 60 diacritics diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/ccitt/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..66a30325 Binary files /dev/null and b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..25cb8633 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1,2 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica +Detected 60 diacritics diff --git a/tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/ccitt/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin deleted file mode 100644 index 3025e665..00000000 --- a/tests/cache/multipage/__-l__eng__000002.ocr.png__000002.text__pdf__txt/txt.bin +++ /dev/null @@ -1,3 +0,0 @@ -Q9OO0Ox9O000 pixels at GOO DPI -S|] megapixels - \ No newline at end of file diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin similarity index 50% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin index 4d626250..2c989a38 100644 --- a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/hocr.bin +++ b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/hocr.bin @@ -4,216 +4,216 @@ - - + + - - -
+ + +

- Replacement - of - "creationism" - with - "intelligent - design" + Replacement + of + "creationism" + with + "intelligent + design"

- +

- + - +

- +

- +

- +

- +

- +

- 120 + 120 - 100 - - + 100 + - - - Cc - 80 + + Cc + 80 - > + > - 5 + 5 - 5 - 607 - —@— - "Creation" - and - "creationist" + 5 + 607 + —@— + "Creation" + and + "creationist" - 5 - —@— - "Intelligent - design" + 5 + —@— + "Intelligent + design" - = - and - "design - proponent" + = + and + "design + proponent" - 40 - - + 40 + - - 20 - - + 20 + - - - —@— - —@® + + —@— + —@® - - 0 - e— - T - T - T - ' - w - ° + + 0 + e— + T + T + T + ' + w + ° - - gp) - ee) - 0 - oN - g\ - gD) - op) + + gp) + ee) + 0 + oN + g\ + gD) + op) - - oO - NC) - NC) - LN - eo - N - N + + oO + NC) + NC) + LN + eo + N + N - - S - os - o* - vs - ws - os - os + + S + os + o* + vs + ws + os + os - - cs) - Re - ss - & - ow - x - & - s? + + cs) + Re + ss + & + ow + x + & + s? - - ge - ee - Oo - ss - Ss - Ne - qs + + ge + ee + Oo + ss + Ss + Ne + qs - G - % - S - S - © - S + G + % + S + S + © + S - Ros - % - se - se - oe - AN + Ros + % + se + se + oe + AN - - Ss - 3s - S - Ss - Ss - Ss - Ss + + Ss + 3s + S + Ss + Ss + Ss + Ss - ow? - \O - Xo) - R - g - g - Q + ow? + \O + Xo) + R + g + g + Q

diff --git a/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000004.ocr.png__000004__hocr__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/txt.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003__hocr__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin similarity index 60% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin index 842a84b6..82d5f141 100644 Binary files a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/pdf.bin and b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000003.ocr.png__000003.text__pdf__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin similarity index 56% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin index 755b0b97..182a705a 100644 --- a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/hocr.bin +++ b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/hocr.bin @@ -4,194 +4,194 @@ - - + + - - -
+ + +

- Replacement - of - "creationism" - with - "intelligent - design" + Replacement + of + "creationism" + with + "intelligent + design"

- +

- + - +

- +

- +

- +

- +

- +

- 120 + 120 - 100 - 4 + 100 + 4 - = - 80 + = + 80 - — + — - S + S - _ - 6047 - —@— - "Creation" - and - "creationist" + _ + 6047 + —@— + "Creation" + and + "creationist" - 5 - —@— - "Intelligent - design" + 5 + —@— + "Intelligent + design" - and - "design - proponent" + and + "design + proponent" - S - «4 + S + «4 - 20 - - + 20 + - - - 0 - oe - I - T - T - T - T - © + + 0 + oe + I + T + T + T + T + © - - 3) - ©) - Ay - Ay - Ay - 9 - o>) + + 3) + ©) + Ay + Ay + Ay + 9 + o>) - - ee - ow - oe - oe - Cs - Cs - eS + + ee + ow + oe + oe + Cs + Cs + eS - - RQ - Q - R - R - XR - Q - R + + RQ + Q + R + R + XR + Q + R - - & - es - o - a - al - & - eo + + & + es + o + a + al + & + eo - - 3 - e - oe - we - a? - i) - as? + + 3 + e + oe + we + a? + i) + as? - - 3 - 4? - 3 - & - oe - & - & + + 3 + 4? + 3 + & + oe + & + & - - oe - ww - 6 - e - Qe - Qe - Qe + + oe + ww + 6 + e + Qe + Qe + Qe

diff --git a/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000003.ocr.png__000003__hocr__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/txt.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004__hocr__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin similarity index 64% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin index 5262d6da..a5591cce 100644 Binary files a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/pdf.bin and b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000003.ocr.png__000003.text__pdf__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000004.ocr.png__000004.text__pdf__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/pdf.bin deleted file mode 100644 index 397f3209..00000000 Binary files a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/hocr.bin similarity index 56% rename from tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/hocr.bin rename to tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/hocr.bin index 3510c868..19e890e9 100644 --- a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/hocr.bin +++ b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/hocr.bin @@ -4,694 +4,694 @@ - - + + - - -
+ + +

- with - a - plain - face, - on - the - throne - of - England; + with + a + plain + face, + on + the + throne + of + England; - there - were - a - king - with - a - large - jaw - and - a - queen + there + were + a + king + with + a + large + jaw + and + a + queen - with - a - fair - face, - on - the - throne - of - France. - In - both + with + a + fair + face, + on + the + throne + of + France. + In + both - countries - it - was - clearer - than - crystal - to - the - lords + countries + it + was + clearer + than + crystal + to + the + lords - of - the - State - preserves - of - loaves - and - fishes, - that + of + the + State + preserves + of + loaves + and + fishes, + that - things - in - general - were - settled - for - ever. + things + in + general + were + settled + for + ever.

- It - was - the - year - of - Our - Lord - one - thousand + It + was + the + year + of + Our + Lord + one + thousand - seven - hundred - and - seventy-five. - Spiritual - reve- + seven + hundred + and + seventy-five. + Spiritual + reve- - lations - were - conceded - to - England - at - that + lations + were + conceded + to + England + at + that - favoured - period, - as - at - this, - Mrs. - Southcott - had + favoured + period, + as + at + this, + Mrs. + Southcott + had - recently - attained - her - five-and-twentieth - blessed + recently + attained + her + five-and-twentieth + blessed - birthday, - of - whom - a - prophetic - private - in - the - Life + birthday, + of + whom + a + prophetic + private + in + the + Life - Guards - had - heralded - the - sublime - appearance - by + Guards + had + heralded + the + sublime + appearance + by - announcing - that - arrangements - were - made - for - the + announcing + that + arrangements + were + made + for + the - swallowing - up - of - London - and - Westminster. + swallowing + up + of + London + and + Westminster. - Even - the - Cock-lane - ghost - had - been - laid - only - a + Even + the + Cock-lane + ghost + had + been + laid + only + a - round - dozen - of - years, - after - rapping - out - its - mes- + round + dozen + of + years, + after + rapping + out + its + mes- - sages, - as - the - spirits - of - this - very - year - last - past + sages, + as + the + spirits + of + this + very + year + last + past - (supematurally - deficient - in - originality) - rapped + (supematurally + deficient + in + originality) + rapped - out - theirs. - Mere - messages - in - the - earthly - order - of + out + theirs. + Mere + messages + in + the + earthly + order + of - events - had - lately - come - to - the - English - Crown - and + events + had + lately + come + to + the + English + Crown + and - People, - from - a - congress - of - British - subjects - in + People, + from + a + congress + of + British + subjects + in - America: - which, - strange - to - relate, - have - proved + America: + which, + strange + to + relate, + have + proved - more - important - to - the - human - race - than - any - com- + more + important + to + the + human + race + than + any + com- - munications - yet - received - through - any - of - the + munications + yet + received + through + any + of + the - chickens - of - the - Cock-lane - brood. + chickens + of + the + Cock-lane + brood.

- France, - less - favoured - on - the - whole - as - to - mat- + France, + less + favoured + on + the + whole + as + to + mat- - ters - spiritual - than - her - sister - of - the - shield - and - tri- + ters + spiritual + than + her + sister + of + the + shield + and + tri- - dent, - rolled - with - exceeding - smoothness - down + dent, + rolled + with + exceeding + smoothness + down - hill, - making - paper - money - and - spending - it. - Under + hill, + making + paper + money + and + spending + it. + Under - the - guidance - of - her - Christian - pastors, - she - enter- + the + guidance + of + her + Christian + pastors, + she + enter- - tained - herself, - besides, - with - such - humane + tained + herself, + besides, + with + such + humane - achievements - as - sentencing - a - youth - to - have - his + achievements + as + sentencing + a + youth + to + have + his

- hands - cut - off, - his - tongue - torn - out - with - pincers, + hands + cut + off, + his + tongue + torn + out + with + pincers, - and - his - body - burned - alive, - because - he - had - not + and + his + body + burned + alive, + because + he + had + not - kneeled - down - in - the - rain - to - do - honour - to - a - dirty + kneeled + down + in + the + rain + to + do + honour + to + a + dirty - procession - of - monks - which - passed - within - his + procession + of + monks + which + passed + within + his - view, - at - a - distance - of - some - fifty - or - sixty - yards. - It + view, + at + a + distance + of + some + fifty + or + sixty + yards. + It - is - likely - enough - that, - rooted - in - the - woods - of + is + likely + enough + that, + rooted + in + the + woods + of - France - and - Norway, - there - were - growing - trees, + France + and + Norway, + there + were + growing + trees, - when - that - sufferer - was - put - to - death, - already + when + that + sufferer + was + put + to + death, + already - marked - by - the - Woodman, - Fate, - to - come - down + marked + by + the + Woodman, + Fate, + to + come + down - and - be - sawn - into - boards, - to - make - a - certain - mov- + and + be + sawn + into + boards, + to + make + a + certain + mov- - able - framework - with - a - sack - and - a - knife - in - it, - ter- + able + framework + with + a + sack + and + a + knife + in + it, + ter- - rible - in - history. - It - is - likely - enough - that - in - the + rible + in + history. + It + is + likely + enough + that + in + the - rough - outhouses - of - some - tillers - of - the - heavy + rough + outhouses + of + some + tillers + of + the + heavy - lands - adjacent - to - Paris, - there - were - sheltered + lands + adjacent + to + Paris, + there + were + sheltered - from - the - weather - that - very - day, - rude - carts, + from + the + weather + that + very + day, + rude + carts, - bespattered - with - rustic - mire, - snuffed - about - by + bespattered + with + rustic + mire, + snuffed + about + by - pigs, - and - roosted - in - by - poultry, - which - the + pigs, + and + roosted + in + by + poultry, + which + the - Farmer, - Death, - had - already - set - apart - to - be - his + Farmer, + Death, + had + already + set + apart + to + be + his - tumbrils - of - the - Revolution. - But - that - Woodman + tumbrils + of + the + Revolution. + But + that + Woodman - and - that - Farmer, - though - they - work - unceasingly, + and + that + Farmer, + though + they + work + unceasingly, - work - silently, - and - no - one - heard - them - as - they + work + silently, + and + no + one + heard + them + as + they - went - about - with - muffled - tread: - the - rather, - foras- + went + about + with + muffled + tread: + the + rather, + foras- - much - as - to - entertain - any - suspicion - that - they + much + as + to + entertain + any + suspicion + that + they - were - awake, - was - to - be - atheistical - and - traitorous. + were + awake, + was + to + be + atheistical + and + traitorous.

- In - England, - there - was - scarcely - an - amount - of + In + England, + there + was + scarcely + an + amount + of - order - and - protection - to - justify - much - national + order + and + protection + to + justify + much + national - boasting. - Daring - burglaries - by - armed - men, - and + boasting. + Daring + burglaries + by + armed + men, + and - highway - robberies, - took - place - in - the - capital + highway + robberies, + took + place + in + the + capital - itself - every - night; - families - were - publicly - cau- + itself + every + night; + families + were + publicly + cau- - tioned - not - to - go - out - of - town - without - removing + tioned + not + to + go + out + of + town + without + removing - their - furniture - to - upholsterers' - warehouses - for + their + furniture + to + upholsterers' + warehouses + for - security; - the - highwayman - in - the - dark - was - a - City + security; + the + highwayman + in + the + dark + was + a + City - tradesman - in - the - light, - and, - being - recognised - and + tradesman + in + the + light, + and, + being + recognised + and

diff --git a/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000002.ocr.png__000002__hocr__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/txt.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000005.ocr.png__000005__hocr__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..99d3c4eb Binary files /dev/null and b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000002.ocr.png__000002.text__pdf__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000005.ocr.png__000005.text__pdf__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/pdf.bin deleted file mode 100644 index 2fa051d2..00000000 Binary files a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/hocr.bin deleted file mode 100644 index 4f7b0583..00000000 --- a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/hocr.bin +++ /dev/null @@ -1,701 +0,0 @@ - - - - - - - - - - -
-
-

- - with - a - plain - face, - on - the - throne - of - England; - - - there - were - a - king - with - a - large - jaw - and - a - queen - - - with - a - fair - face, - on - the - throne - of - France. - In - both - - - countries - it - was - clearer - than - crystal - to - the - lords - - - of - the - State - preserves - of - loaves - and - fishes, - that - - - things - in - general - were - settled - for - ever. - -

-
-
-

- - It - was - the - year - of - Our - Lord - one - thousand - - - seven - hundred - and - seventy-five. - Spiritual - reve- - - - lations - were - conceded - to - England - at - that - - - favoured - period, - as - at - this. - Mrs. - Southcott - had - - - recently - attained - her - five-and-twentieth - blessed - - - birthday, - of - whom - a - prophetic - private - in - the - Life - - - Guards - had - heralded - the - sublime - appearance - by - - - announcing - that - arrangements - were - made - for - the - - - swallowing - up - of - London - and - Westminster. - - - Even - the - Cock-lane - ghost - had - been - laid - only - a - - - round - dozen - of - years, - after - rapping - out - its - mes- - - - sages, - as - the - spirits - of - this - very - year - last - past - - - (supernaturally - deficient - in - originality) - rapped - - - out - theirs. - Mere - messages - in - the - earthly - order - of - - - events - had - lately - come - to - the - English - Crown - and - - - People, - from - a - congress - of - British - subjects - in - - - America: - which, - strange - to - relate, - have - proved - - - more - important - to - the - human - race - than - any - com- - - - munications - yet - received - through - any - of - the - - - chickens - of - the - Cock-lane - brood. - -

-
-
-

- - France, - less - favoured - on - the - whole - as - to - mat- - - - ters - spiritual - than - her - sister - of - the - shield - and - tri- - - - dent, - rolled - with - exceeding - smoothness - down - - - hill, - making - paper - money - and - spending - it. - Under - - - the - guidance - of - her - Christian - pastors, - she - enter- - - - tained - herself, - besides, - with - such - humane - - - achievements - as - sentencing - a - youth - to - have - his - -

-
-
-

- - hands - cut - off, - his - tongue - torn - out - with - pincers, - - - and - his - body - burned - alive, - because - he - had - not - - - kneeled - down - in - the - rain - to - do - honour - to - a - dirty - - - procession - of - monks - which - passed - within - his - - - view, - at - a - distance - of - some - fifty - or - sixty - yards. - It - - - is - likely - enough - that, - rooted - in - the - woods - of - - - France - and - Norway, - there - were - growing - trees, - - - when - that - sufferer - was - put - to - death, - already - - - marked - by - the - Woodman, - Fate, - to - come - down - - - and - be - sawn - into - boards, - to - make - a - certain - mov- - - - able - framework - with - a - sack - and - a - knife - in - it, - ter- - - - rible - in - history. - It - is - likely - enough - that - in - the - - - rough - outhouses - of - some - tillers - of - the - heavy - - - lands - adjacent - to - Paris, - there - were - sheltered - - - from - the - weather - that - very - day, - rude - carts, - - - bespattered - with - rustic - mire, - snuffed - about - by - - - pigs, - and - roosted - in - by - poultry, - which - the - - - Farmer, - Death, - had - already - set - apart - to - be - his - - - tumbrils - of - the - Revolution. - But - that - Woodman - - - and - that - Farmer, - though - they - work - unceasingly, - - - work - silently, - and - no - one - heard - them - as - they - - - went - about - with - muffled - tread: - the - rather, - foras- - - - much - as - to - entertain - any - suspicion - that - they - - - were - awake, - was - to - be - atheistical - and - traitorous. - -

-
-
-

- - In - England, - there - was - scarcely - an - amount - of - - - order - and - protection - to - justify - much - national - - - boasting. - Daring - burglaries - by - armed - men, - and - - - highway - robberies, - took - place - in - the - capital - - - itself - every - night; - families - were - publicly - cau- - - - tioned - not - to - go - out - of - town - without - removing - - - their - furniture - to - upholsterers' - warehouses - for - - - security; - the - highwayman - in - the - dark - was - a - City - - - tradesman - in - the - light, - and, - being - recognised - and - -

-
-
- - diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/hocr.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/hocr.bin new file mode 100644 index 00000000..3e3c4012 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/hocr.bin @@ -0,0 +1,701 @@ + + + + + + + + + + +
+
+

+ + with + a + plain + face, + on + the + throne + of + England; + + + there + were + a + king + with + a + large + jaw + and + a + queen + + + with + a + fair + face, + on + the + throne + of + France. + In + both + + + countries + it + was + clearer + than + crystal + to + the + lords + + + of + the + State + preserves + of + loaves + and + fishes, + that + + + things + in + general + were + settled + for + ever. + +

+
+
+

+ + It + was + the + year + of + Our + Lord + one + thousand + + + seven + hundred + and + seventy-five. + Spiritual + reve- + + + lations + were + conceded + to + England + at + that + + + favoured + period, + as + at + this. + Mrs. + Southcott + had + + + recently + attained + her + five-and-twentieth + blessed + + + birthday, + of + whom + a + prophetic + private + in + the + Life + + + Guards + had + heralded + the + sublime + appearance + by + + + announcing + that + arrangements + were + made + for + the + + + swallowing + up + of + London + and + Westminster. + + + Even + the + Cock-lane + ghost + had + been + laid + only + a + + + round + dozen + of + years, + after + rapping + out + its + mes- + + + sages, + as + the + spirits + of + this + very + year + last + past + + + (supernaturally + deficient + in + originality) + rapped + + + out + theirs. + Mere + messages + in + the + earthly + order + of + + + events + had + lately + come + to + the + English + Crown + and + + + People, + from + a + congress + of + British + subjects + in + + + America: + which, + strange + to + relate, + have + proved + + + more + important + to + the + human + race + than + any + com- + + + munications + yet + received + through + any + of + the + + + chickens + of + the + Cock-lane + brood. + +

+
+
+

+ + France, + less + favoured + on + the + whole + as + to + mat- + + + ters + spiritual + than + her + sister + of + the + shield + and + tri- + + + dent, + rolled + with + exceeding + smoothness + down + + + hill, + making + paper + money + and + spending + it. + Under + + + the + guidance + of + her + Christian + pastors, + she + enter- + + + tained + herself, + besides, + with + such + humane + + + achievements + as + sentencing + a + youth + to + have + his + +

+
+
+

+ + hands + cut + off, + his + tongue + torn + out + with + pincers, + + + and + his + body + burned + alive, + because + he + had + not + + + kneeled + down + in + the + rain + to + do + honour + to + a + dirty + + + procession + of + monks + which + passed + within + his + + + view, + at + a + distance + of + some + fifty + or + sixty + yards. + It + + + is + likely + enough + that, + rooted + in + the + woods + of + + + France + and + Norway, + there + were + growing + trees, + + + when + that + sufferer + was + put + to + death, + already + + + marked + by + the + Woodman, + Fate, + to + come + down + + + and + be + sawn + into + boards, + to + make + a + certain + mov- + + + able + framework + with + a + sack + and + a + knife + in + it, + ter- + + + rible + in + history. + It + is + likely + enough + that + in + the + + + rough + outhouses + of + some + tillers + of + the + heavy + + + lands + adjacent + to + Paris, + there + were + sheltered + + + from + the + weather + that + very + day, + rude + carts, + + + bespattered + with + rustic + mire, + snuffed + about + by + + + pigs, + and + roosted + in + by + poultry, + which + the + + + Farmer, + Death, + had + already + set + apart + to + be + his + + + tumbrils + of + the + Revolution. + But + that + Woodman + + + and + that + Farmer, + though + they + work + unceasingly, + + + work + silently, + and + no + one + heard + them + as + they + + + went + about + with + muffled + tread: + the + rather, + foras- + + + much + as + to + entertain + any + suspicion + that + they + + + were + awake, + was + to + be + atheistical + and + traitorous. + +

+
+
+

+ + In + England, + there + was + scarcely + an + amount + of + + + order + and + protection + to + justify + much + national + + + boasting. + Daring + burglaries + by + armed + men, + and + + + highway + robberies, + took + place + in + the + capital + + + itself + every + night; + families + were + publicly + cau- + + + tioned + not + to + go + out + of + town + without + removing + + + their + furniture + to + upholsterers' + warehouses + for + + + security; + the + highwayman + in + the + dark + was + a + City + + + tradesman + in + the + light, + and, + being + recognised + and + +

+
+
+ + diff --git a/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/txt.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000006.ocr.png__000006__hocr__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/pdf.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..4a339f17 Binary files /dev/null and b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/stderr.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/cardinal/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/txt.bin b/tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/multipage/__-l__eng__000006.ocr.png__000006.text__pdf__txt/txt.bin rename to tests/cache/multipage/__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index 68839285..00000000 Binary files a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index cdcfe62c..00000000 --- a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1,2 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica -Detected 180 diacritics diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index cdcfe62c..00000000 --- a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1,2 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica -Detected 180 diacritics diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 54% rename from tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index 19cb94af..fd60c85f 100644 --- a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,423 +4,423 @@ - - + + - - -
+ + +

- 41st - CONGRESS, - } - SENATE. + 41st + CONGRESS, + } + SENATE. - - 3d - Session. + + 3d + Session.

- MESSAGE + MESSAGE

- OF - THE + OF + THE

- PRESIDENT - OF - THE - UNITED - STATES + PRESIDENT + OF + THE + UNITED + STATES

- A - copy - of - regulations - for - the - consular - courts - of - the - United - States - in - Japan, + A + copy + of + regulations + for + the + consular + courts + of + the + United + States + in + Japan, - decreed - and - issued - by - the - minister - of - the - United - States - in - that - country. + decreed + and + issued + by + the + minister + of + the + United + States + in + that + country.

- Janvary - 27, - 1871—Read, - referred - to - the - Committee - on - Commerce, - and - ordered - to - be + Janvary + 27, + 1871—Read, + referred + to + the + Committee + on + Commerce, + and + ordered + to + be - printed, + printed,

- To - the - Senate - and - House - of - Representatives - : + To + the + Senate + and + House + of + Representatives + :

- I - transmit - herewith, - for - the - consideration - of - Congress, - a - report - from + I + transmit + herewith, + for + the + consideration + of + Congress, + a + report + from - the - Secretary - of - State, - and - the - papers - which - accompanied - it, - concern- + the + Secretary + of + State, + and + the + papers + which + accompanied + it, + concern- - ing - regulations - for - the - consular - courts - of - the - United - States - in - Ja + ing + regulations + for + the + consular + courts + of + the + United + States + in + Ja

- U. - 8. - GRAN + U. + 8. + GRAN

- WASHINGTON, - January - 27, - 1871. + WASHINGTON, + January + 27, + 1871.

- DEPARTMENT - OF - STATE, + DEPARTMENT + OF + STATE, - Washington, - January - 26, - 1870. + Washington, + January + 26, + 1870. - The - Secretary - of - State - has - the - honor - to - submit - herewith, - for - revision + The + Secretary + of + State + has + the + honor + to + submit + herewith, + for + revision - by - Congress, - in - conformity - with - the - provisions - of - section - 6 - of - the - act + by + Congress, + in + conformity + with + the + provisions + of + section + 6 + of + the + act - approved - 22d - of - June, - 1860, - a - copy - of - “regulations - for - the - consular + approved + 22d + of + June, + 1860, + a + copy + of + “regulations + for + the + consular - courts - of - the - United - States - in - Japan,” - decreed - and - issued - by - C. - E. + courts + of + the + United + States + in + Japan,” + decreed + and + issued + by + C. + E. - De - Long, - the - minister - of - the - United - States - in - that - country, - in - Septem- + De + Long, + the + minister + of + the + United + States + in + that + country, + in + Septem- - ber, - 1870; - and - also - the - papers - mentioned - in - the - subjoined - list, - which + ber, + 1870; + and + also + the + papers + mentioned + in + the + subjoined + list, + which - contain - suggestions - on - the - subject - thereof. - : + contain + suggestions + on + the + subject + thereof. + : - A - copy - of - Art - XVI - of - the - consist - regulations - so - Submitted, + A + copy + of + Art + XVI + of + the + consist + regulations + so + Submitted, - and - the - Secretary - of - r - i - ly - , - for - the - consideration + and + the + Secretary + of + r + i + ly + , + for + the + consideration - of - ministers - to - make + of + ministers + to + make - ion, - in - the - sense - in - which - it - is - limited - by - paragraph + ion, + in + the + sense + in + which + it + is + limited + by + paragraph - 431 - of - the - e - be: - fore - named—that - is, - “ - to - acts - necessary - to - organize + 431 + of + the + e + be: + fore + named—that + is, + “ + to + acts + necessary + to + organize - and - give - efficiency - to - the - courts - created - by - the - act.” + and + give + efficiency + to + the + courts + created + by + the + act.” - - Respectful - submitted. + + Respectful + submitted. - HAMILTON - FISH. + HAMILTON + FISH. - The - PRESIDENT. + The + PRESIDENT.

- List - of - accompanying - papers. + List + of + accompanying + papers.

- 1. - Regulations - for - the - consular - courts - of - the - United - States - in - Japan. + 1. + Regulations + for + the + consular + courts + of + the + United + States + in + Japan. - 2. - Mr. - Fish - to - Mr. - De - Long, - September - 10, - 1870, + 2. + Mr. + Fish + to + Mr. + De + Long, + September + 10, + 1870,

- +

diff --git a/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..46c06168 --- /dev/null +++ b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1,2 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica +Detected 180 diacritics diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin similarity index 100% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001__hocr__txt/stdout.bin rename to tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/palette/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..55a7fbdf Binary files /dev/null and b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..46c06168 --- /dev/null +++ b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1,2 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica +Detected 180 diacritics diff --git a/tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/aspect/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/palette/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/palette/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index c09b9eb3..00000000 Binary files a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..33a54256 Binary files /dev/null and b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin similarity index 100% rename from tests/cache/2400dpi/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stdout.bin rename to tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin diff --git a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 91% rename from tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin index 34a37290..b3381922 100644 --- a/tests/cache/poster/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin +++ b/tests/cache/poster/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin @@ -12,7 +12,7 @@ be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic synthesizers! -© Ultra-fast 3!” disk drive stores complex songs in seconds and holds over 110,000 notes +¢ Ultra-fast 3!” disk drive stores complex songs in seconds and holds over 110,000 notes per disk! @@ -33,7 +33,7 @@ Recording a Sequence To record a sequence, simply press RECORD and PLAY, then play your MIDI keyboard in time to the Sequencer’s click track. When the sequence loops back around to bar 1, -you’ll hear what you played—only all timing errors will be +you’ ll hear what you played—only all timing errors will be corrected! (Timing correction may be adjusted or defeated). Any additional notes played will be added into the track —existing notes are not erased while recording! @@ -43,7 +43,6 @@ may be used at any time to quickly access any location in your sequence for spot-recording. To overdub a new part, select a different track and start recording—while you record, the first track will play in perfect sync (unless you - MUTE it, or SOLO another track). In this way, up to 32 tracks may be overdubbed! All MIDI effects are recorded including pitch bend, modulation, velocity, aftertouch, @@ -91,7 +90,7 @@ music. See your Linn dealer today for a demonstration! HELP button displays additional explanations. -© Non-destructive recording—existing notes are not erased while recording. +® Non-destructive recording—existing notes are not erased while recording. © Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including ERASE, REPEAT, PLAY/STOP, or LOCATE. @@ -100,7 +99,7 @@ ERASE, REPEAT, PLAY/STOP, or LOCATE. e Will sync to standard LinnDrum or Linn 9000 sync tone. -Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. +© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation. ¢ TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second, (even drop frame!) @@ -109,7 +108,7 @@ Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST opera on the TAP TEMPO button. -¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired. +e TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired. e Any TIME SIGNATURE may be used, and may be changed within a song. linn diff --git a/tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin b/tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin deleted file mode 100644 index 2d1bd1c9..00000000 --- a/tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Warning. Invalid resolution 0 dpi. Using 70 instead. diff --git a/tests/cache/poster/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stderr.bin b/tests/cache/poster/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stderr.bin new file mode 100644 index 00000000..e69de29b diff --git a/tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stdout.bin b/tests/cache/poster/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stdout.bin similarity index 54% rename from tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stdout.bin rename to tests/cache/poster/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stdout.bin index d02007b8..12ca335f 100644 --- a/tests/cache/poster/__-l__osd__--psm__0__000001.ocr.preview.jpg__stdout/stdout.bin +++ b/tests/cache/poster/__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout/stdout.bin @@ -1,6 +1,6 @@ Page number: 0 Orientation in degrees: 0 Rotate: 0 -Orientation confidence: 29.83 +Orientation confidence: 32.36 Script: Latin -Script confidence: 1.88 +Script confidence: 4.54 diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin similarity index 54% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/hocr.bin rename to tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin index ada2aee1..43520b39 100644 --- a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/hocr.bin +++ b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -4,12 +4,12 @@ - - + + - - -
+ + +
diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin new file mode 100644 index 00000000..e69de29b diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin similarity index 97% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/pdf.bin rename to tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin index fd936de3..31627deb 100644 Binary files a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/pdf.bin and b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin new file mode 100644 index 00000000..e69de29b diff --git a/tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/skew/__-l__eng__--psm__7__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/skew/__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin b/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin deleted file mode 100644 index 634dc61b..00000000 Binary files a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/pdf.bin and /dev/null differ diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin b/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin b/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin deleted file mode 100644 index 87f8ad9b..00000000 --- a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/hocr.bin +++ /dev/null @@ -1,973 +0,0 @@ - - - - - - - - - - -
-
-

- - The - LinnSequencer - - - 32 - Track - MIDI - Sequence - Recorder - -

-
-
-

- - The - LinnSequencer - is - a - state-of-the-art - composition - and - performance - tool - for - the - professional - musician. - It - is - - - extremely - powerful, - yet - amazingly - simple - to - learn - and - use. - It - ’s - many - remarkable - features - include: - -

-
-
-

- - * - Operation - is - similar - to - multi-track - tape - recorder - with - PLAY, - STOP, - RECORD, - FAST - - - FORWARD, - REWIND, - and - LOCATE - controls, - -

-
-
-

- - © - Each - of - the - 100 - sequences - contains - 32 - simultaneous, - polyphonic - tracks. - Each - track - may - - - be - assigned - to - one - of - 16 - MIDI - channels. - Simultaneously - plays - up - to - 16 - polyphonic - - - synthesizers! - -

-
-
-

- - * - Ultra-fast - 314" - disk - drive - stores - complex - songs - in - seconds - and - holds - over - 110,000 - notes - - - per - disk! - -

-
-
-

- - * - One - or - all - tracks - may - be - TRANSPOSED - at - the - touch - of - a - key. - - - ¢ - Exclusive - real-time - ERASE - function - makes - editing - FAST, - -

-
-
-

- - * - Exclusive - REPEAT - function - automatically - repeats - any - held - notes - at - a - pre-selected - - - rhythmic - value. - -

-
-
-

- - * - TIMING - CORRECTION - works - during - playback - and - operates - without - ‘chopping’ - notes, - - - ¢ - Optional - SMPTE - time - code - synchronization. - - - * - Optional - remote - control. - -

-
-
-

- - Recording - a - Sequence - simply - use - LOCATE, - FAST - FORWARD, - or - REWIND - to - - - To - record - a - sequence, - simply - press - RECORD - and - PL - AY, - _ - find - the - desired - bar - number, - then - Start - recording. - - - then - play - your - MIDI - keyboard - in - time - to - the - Sequencer’s - The - INSERT/COPY - function - allows - you - to - move - bars - -

-
-
-

- - click - track, - When - the - sequence - loops - back - around - to - bar - 1, - from - one - location - to - another—in - the - same - sequence - or - a - - - you'll - hear - what - you - played—only - all - timing - errors - will - be - _different - one. - For - example, - you - might - insert - a - copy - of - the - - - corrected! - (Timing - correction - may - be - adjusted - or - defeated), - _ - first - verse - between - the - second - chorus - and - the - bridge. - - - Any - additional - notes - played - will - be - added - into - the - track - DELETE - BARS - operates - the - same - way - to - remove - - - —existing - notes - are - not - erased - while - recording! - unwanted - sections, - -

- -

- - FAST - FORWARD, - REWIND, - and - LOCATE - controls - . - - - may - be - used - at - any - time - to - quickly - access - any - location - in - Creating - a - Song - -

-
-
-

- - your - sequence - for - spot-recording. - To - overdub - a - new - part, - One - way - to - create - a - song - is - to - record - each - track - all - the - - - select - a - different - track - and - start - recording—while - you - way - through - (up - to - 999 - bars), - Another - way - is - to - record - - - record, - the - first - track - will - play - in - perfect - sync - (unless - you - each - basic - section - (verse, - chorus, - etc.) - in - individual - -

-
-
-

- - MUTE - it, - or - SOLO - another - track). - In - this - way, - up - to - 32 - sequences, - then - use - the - CREATE - SONG - function - to - “chain” - - - tracks - may - be - overdubbed! - All - MIDI - effects - are - recorded - them - together. - CREATE - SONG - will - then - automatically - -

-
-
-

- - including - pitch - bend, - modulation, - velocity, - aftertouch, - Copy - all - the - parts - into - a - new - sequence. - If - desir - ed, - you - can - - - sustain - pedal, - and - program - changes! - even - set - the - last - few - bars - to - repeat - infinitely, - for - a - fadeout. - - - Editing - Composition - Without - Compromise - -

- -

- - To - erase - a - wrong - note, - simply - hold - ERASE - and - press - The - technology - you - use - should - never - be - so - complex - that - - - the - note - to - be - erased - just - before - it - plays - in - the - sequence— - it - interferes - with - the - creative - process. - That’s - precisely - why - - - when - played - back, - it - will - be - gone. - Notes - may - also - be - the - LinnSequencer - is - designed - to - let - you - compose, - record - -

-
-
-

- - added, - erased, - or - changed - using - the - SINGLE - STEP - func- - and - edit - while - devoting - your - undivided - attention - to - your - - - tion. - To - overdub - notes - at - specific - points - within - a - Sequence, - — - music. - See - your - Linn - dealer - today - for - a - demonstration! - -

-
-
-

- - Additional - Features - -

-
-
-

- - * - Simple, - easy - to - learn - operation—the - 32 - character - LCD - display - clearly - guides - you - through - all - operations, - If - needed, - the - - - HELP - button - displays - additional - explanations, - -

-
-
-

- - * - Non-destructive - recording—existing - notes - are - not - erased - while - recording. - -

-
-
-

- - * - Two - FOOTSWITCH - INPUTS - may - be - assigned - to - remotely - control - many - of - the - commonly - used - functions, - including - - - ERASE, - REPEAT, - PLAY/ - STOP, - or - LOCATE, - -

-
-
-

- - * - Two - TRIGGER - OUTPUTS - may - be - programmed - to - output - pulses - at - any - selected - note - value. - - - © - Will - sync - to - standard - LinnDrum - or - Linn - 9000 - sync - tone, - - - * - Utilizes - ultra - high-speed, - 8 - MHz - 80186 - 16 - bit - computer - internally - for - FAST - operation. - -

-
-
-

- - * - TEMPO - may - be - specified - in - BEATS-PER-MINUTE - or - FRAMES-PER-BEAT - at - 24, - 25, - or - 30 - frames - per - second, - - - (even - drop - frame!) - -

-
-
-

- - * - TEMPO - may - be - entered - numerically, - adjustable - in - tenths - of - a - Beat-Per-Minute - increments, - or - by - tapping - quarter - notes - - - on - the - TAP - TEMPO - button. - -

-
-
-

- - * - TEMPO - CHANGES - may - be - programmed - into - a - sequence, - with - smooth - transitions - if - desired. - -

-
-
-

- - « - Any - TIME - SIGNATURE - may - be - used, - and - may - be - changed - within - a - song. - - - linn - -

-
-
-

- - Linn - Electronics, - Inc. - - - 18720 - Oxnard - Street, - Tarzana, - CA - 91356 - - - (818) - 708-8131 - TELEX - #298949 - LINN - UR - -

-
-
- - diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin b/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin deleted file mode 100644 index 61f78d82..00000000 --- a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/stderr.bin +++ /dev/null @@ -1 +0,0 @@ -Tesseract Open Source OCR Engine v4.0.0 with Leptonica diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin new file mode 100644 index 00000000..17df1412 --- /dev/null +++ b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/hocr.bin @@ -0,0 +1,973 @@ + + + + + + + + + + +
+
+

+ + The + LinnSequencer + + + 32 + Track + MIDI + Sequence + Recorder + +

+
+
+

+ + The + LinnSequencer + is + a + state-of-the-art + composition + and + performance + tool + for + the + professional + musician. + It + is + + + extremely + powerful, + yet + amazingly + simple + to + learn + and + use. + It + ’s + many + remarkable + features + include: + +

+
+
+

+ + * + Operation + is + similar + to + multi-track + tape + recorder + with + PLAY, + STOP, + RECORD, + FAST + + + FORWARD, + REWIND, + and + LOCATE + controls, + +

+
+
+

+ + © + Each + of + the + 100 + sequences + contains + 32 + simultaneous, + polyphonic + tracks. + Each + track + may + + + be + assigned + to + one + of + 16 + MIDI + channels. + Simultaneously + plays + up + to + 16 + polyphonic + + + synthesizers! + +

+
+
+

+ + * + Ultra-fast + 314" + disk + drive + stores + complex + songs + in + seconds + and + holds + over + 110,000 + notes + + + per + disk! + +

+
+
+

+ + * + One + or + all + tracks + may + be + TRANSPOSED + at + the + touch + of + a + key. + + + ¢ + Exclusive + real-time + ERASE + function + makes + editing + FAST, + +

+
+
+

+ + * + Exclusive + REPEAT + function + automatically + repeats + any + held + notes + at + a + pre-selected + + + rhythmic + value. + +

+
+
+

+ + * + TIMING + CORRECTION + works + during + playback + and + operates + without + ‘chopping’ + notes, + + + ¢ + Optional + SMPTE + time + code + synchronization. + + + * + Optional + remote + control. + +

+
+
+

+ + Recording + a + Sequence + simply + use + LOCATE, + FAST + FORWARD, + or + REWIND + to + + + To + record + a + sequence, + simply + press + RECORD + and + PL + AY, + _ + find + the + desired + bar + number, + then + Start + recording. + + + then + play + your + MIDI + keyboard + in + time + to + the + Sequencer’s + The + INSERT/COPY + function + allows + you + to + move + bars + +

+
+
+

+ + click + track, + When + the + sequence + loops + back + around + to + bar + 1, + from + one + location + to + another—in + the + same + sequence + or + a + + + you'll + hear + what + you + played—only + all + timing + errors + will + be + _different + one. + For + example, + you + might + insert + a + copy + of + the + + + corrected! + (Timing + correction + may + be + adjusted + or + defeated), + _ + first + verse + between + the + second + chorus + and + the + bridge. + + + Any + additional + notes + played + will + be + added + into + the + track + DELETE + BARS + operates + the + same + way + to + remove + + + —existing + notes + are + not + erased + while + recording! + unwanted + sections, + +

+ +

+ + FAST + FORWARD, + REWIND, + and + LOCATE + controls + . + + + may + be + used + at + any + time + to + quickly + access + any + location + in + Creating + a + Song + +

+
+
+

+ + your + sequence + for + spot-recording. + To + overdub + a + new + part, + One + way + to + create + a + song + is + to + record + each + track + all + the + + + select + a + different + track + and + start + recording—while + you + way + through + (up + to + 999 + bars), + Another + way + is + to + record + + + record, + the + first + track + will + play + in + perfect + sync + (unless + you + each + basic + section + (verse, + chorus, + etc.) + in + individual + +

+
+
+

+ + MUTE + it, + or + SOLO + another + track). + In + this + way, + up + to + 32 + sequences, + then + use + the + CREATE + SONG + function + to + “chain” + + + tracks + may + be + overdubbed! + All + MIDI + effects + are + recorded + them + together. + CREATE + SONG + will + then + automatically + +

+
+
+

+ + including + pitch + bend, + modulation, + velocity, + aftertouch, + Copy + all + the + parts + into + a + new + sequence. + If + desir + ed, + you + can + + + sustain + pedal, + and + program + changes! + even + set + the + last + few + bars + to + repeat + infinitely, + for + a + fadeout. + + + Editing + Composition + Without + Compromise + +

+ +

+ + To + erase + a + wrong + note, + simply + hold + ERASE + and + press + The + technology + you + use + should + never + be + so + complex + that + + + the + note + to + be + erased + just + before + it + plays + in + the + sequence— + it + interferes + with + the + creative + process. + That’s + precisely + why + + + when + played + back, + it + will + be + gone. + Notes + may + also + be + the + LinnSequencer + is + designed + to + let + you + compose, + record + +

+
+
+

+ + added, + erased, + or + changed + using + the + SINGLE + STEP + func- + and + edit + while + devoting + your + undivided + attention + to + your + + + tion. + To + overdub + notes + at + specific + points + within + a + Sequence, + — + music. + See + your + Linn + dealer + today + for + a + demonstration! + +

+
+
+

+ + Additional + Features + +

+
+
+

+ + * + Simple, + easy + to + learn + operation—the + 32 + character + LCD + display + clearly + guides + you + through + all + operations, + If + needed, + the + + + HELP + button + displays + additional + explanations, + +

+
+
+

+ + * + Non-destructive + recording—existing + notes + are + not + erased + while + recording. + +

+
+
+

+ + * + Two + FOOTSWITCH + INPUTS + may + be + assigned + to + remotely + control + many + of + the + commonly + used + functions, + including + + + ERASE, + REPEAT, + PLAY/ + STOP, + or + LOCATE, + +

+
+
+

+ + * + Two + TRIGGER + OUTPUTS + may + be + programmed + to + output + pulses + at + any + selected + note + value. + + + © + Will + sync + to + standard + LinnDrum + or + Linn + 9000 + sync + tone, + + + * + Utilizes + ultra + high-speed, + 8 + MHz + 80186 + 16 + bit + computer + internally + for + FAST + operation. + +

+
+
+

+ + * + TEMPO + may + be + specified + in + BEATS-PER-MINUTE + or + FRAMES-PER-BEAT + at + 24, + 25, + or + 30 + frames + per + second, + + + (even + drop + frame!) + +

+
+
+

+ + * + TEMPO + may + be + entered + numerically, + adjustable + in + tenths + of + a + Beat-Per-Minute + increments, + or + by + tapping + quarter + notes + + + on + the + TAP + TEMPO + button. + +

+
+
+

+ + * + TEMPO + CHANGES + may + be + programmed + into + a + sequence, + with + smooth + transitions + if + desired. + +

+
+
+

+ + « + Any + TIME + SIGNATURE + may + be + used, + and + may + be + changed + within + a + song. + + + linn + +

+
+
+

+ + Linn + Electronics, + Inc. + + + 18720 + Oxnard + Street, + Tarzana, + CA + 91356 + + + (818) + 708-8131 + TELEX + #298949 + LINN + UR + +

+
+
+ + diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/stdout.bin new file mode 100644 index 00000000..e69de29b diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin similarity index 100% rename from tests/cache/skew/__-l__eng__000001.ocr.png__000001__hocr__txt/txt.bin rename to tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt/txt.bin diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin new file mode 100644 index 00000000..b143cc9b Binary files /dev/null and b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/pdf.bin differ diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin new file mode 100644 index 00000000..16b617e5 --- /dev/null +++ b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stderr.bin @@ -0,0 +1 @@ +Tesseract Open Source OCR Engine v4.1.1 with Leptonica diff --git a/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/stdout.bin new file mode 100644 index 00000000..e69de29b diff --git a/tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin b/tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin similarity index 100% rename from tests/cache/skew/__-l__eng__000001.ocr.png__000001.text__pdf__txt/txt.bin rename to tests/cache/skew/__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt/txt.bin diff --git a/tests/conftest.py b/tests/conftest.py index 19679fdf..9a059c83 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,9 @@ # © 2017 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + import os import platform @@ -23,110 +13,42 @@ from subprocess import PIPE, run import pytest -pytest_plugins = ['helpers_namespace'] +from ocrmypdf import api, pdfinfo +from ocrmypdf._exec import unpaper +from ocrmypdf._plugin_manager import get_parser_options_plugins -try: - from pytest_cov.embed import cleanup_on_sigterm -except ImportError: - pass -else: - cleanup_on_sigterm() - -# pylint: disable=E1101 -# pytest.helpers is dynamic so it confuses pylint - -if sys.version_info.major < 3: - print("Requires Python 3.4+") +if sys.version_info < (3, 5): + print("Requires Python 3.5+") sys.exit(1) -@pytest.helpers.register def is_linux(): return platform.system() == 'Linux' -@pytest.helpers.register def is_macos(): return platform.system() == 'Darwin' -@pytest.helpers.register def running_in_docker(): # Docker creates a file named /.dockerenv (newer versions) or # /.dockerinit (older) -- this is undocumented, not an offical test - return os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit') + return Path('/.dockerenv').exists() or Path('/.dockerinit').exists() -@pytest.helpers.register -def running_in_travis(): - return os.environ.get('TRAVIS') == 'true' - - -@pytest.helpers.register -def needs_pdfminer(fn): - try: - import pdfminer - except ImportError: - skip = pytest.mark.skipif(True, reason="pdfminer not available") - return skip(fn) - return fn - - -@pytest.helpers.register def have_unpaper(): try: - from ocrmypdf.exec import unpaper - unpaper.version() - except Exception: + except Exception: # pylint: disable=broad-except return False return True -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof') -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT OCRMYPDF = [sys.executable, '-m', 'ocrmypdf'] -@pytest.helpers.register -def spoof(tmpdir_factory, **kwargs): - """Modify PATH to override subprocess executables - - spoof(program1='replacement', ...) - - Creates temporary directory with symlinks to targets. - - """ - env = os.environ.copy() - slug = '-'.join(v.replace('.py', '') for v in sorted(kwargs.values())) - spoofer_base = Path(str(tmpdir_factory.mktemp('spoofers'))) - tmpdir = spoofer_base / slug - tmpdir.mkdir(parents=True) - - for replace_program, with_spoof in kwargs.items(): - spoofer = Path(SPOOF_PATH) / with_spoof - spoofer.chmod(0o755) - (tmpdir / replace_program).symlink_to(spoofer) - - env['_OCRMYPDF_SAVE_PATH'] = env['PATH'] - env['PATH'] = str(tmpdir) + ":" + env['PATH'] - - return env - - -@pytest.fixture(scope='session') -def spoof_tesseract_noop(tmpdir_factory): - return spoof(tmpdir_factory, tesseract='tesseract_noop.py') - - -@pytest.fixture(scope='session') -def spoof_tesseract_cache(tmpdir_factory): - if running_in_docker(): - return os.environ.copy() - return spoof(tmpdir_factory, tesseract="tesseract_cache.py") - - @pytest.fixture def resources(): return Path(TESTS_ROOT) / 'resources' @@ -138,66 +60,79 @@ def ocrmypdf_exec(): @pytest.fixture(scope="function") -def outdir(tmpdir): - return Path(str(tmpdir)) +def outdir(tmp_path): + return tmp_path @pytest.fixture(scope="function") -def outpdf(tmpdir): - return str(Path(str(tmpdir)) / 'out.pdf') +def outpdf(tmp_path): + return tmp_path / 'out.pdf' @pytest.fixture(scope="function") -def no_outpdf(tmpdir): +def no_outpdf(tmp_path): """This just documents the fact that a test is not expected to produce output. Unfortunately an assertion failure inside a test fixture produces an error rather than a test failure, so no testing is done. It's up to the test to confirm that no output file was created.""" - return str(Path(str(tmpdir)) / 'no_output.pdf') + return tmp_path / 'no_output.pdf' -@pytest.helpers.register -def check_ocrmypdf(input_file, output_file, *args, env=None): +def check_ocrmypdf(input_file, output_file, *args): """Run ocrmypdf and confirmed that a valid file was created""" + args = [str(input_file), str(output_file)] + [ + str(arg) for arg in args if arg is not None + ] + + _parser, options, plugin_manager = get_parser_options_plugins(args=args) + api.check_options(options, plugin_manager) + result = api.run_pipeline(options, plugin_manager=plugin_manager, api=True) + + assert result == 0 + assert output_file.exists(), "Output file not created" + assert output_file.stat().st_size > 100, "PDF too small or empty" - p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env) - # ensure py.test collects the output, use -s to view - print(err, file=sys.stderr) - assert p.returncode == 0 - assert os.path.exists(str(output_file)), "Output file not created" - assert os.stat(str(output_file)).st_size > 100, "PDF too small or empty" - assert out == "", ( - "The following was written to stdout and should not have been: \n" - + "\n" - + out - + "\n" - ) return output_file -@pytest.helpers.register -def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=True): - "Run ocrmypdf and let caller deal with results" +def run_ocrmypdf_api(input_file, output_file, *args): + """Run ocrmypdf via API and let caller deal with results - if env is None: - env = os.environ + Does not currently have a way to manipulate the PATH except for Tesseract. + """ + + args = [str(input_file), str(output_file)] + [ + str(arg) for arg in args if arg is not None + ] + _parser, options, plugin_manager = get_parser_options_plugins(args=args) + + api.check_options(options, plugin_manager) + return api.run_pipeline(options, plugin_manager=None, api=False) + + +def run_ocrmypdf(input_file, output_file, *args, text=True): + "Run ocrmypdf and let caller deal with results" p_args = ( OCRMYPDF + [str(arg) for arg in args if arg is not None] + [str(input_file), str(output_file)] ) + + env = os.environ.copy() p = run( - p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env + p_args, + stdout=PIPE, + stderr=PIPE, + universal_newlines=text, # When dropping support for Python 3.6 change to text= + env=env, + check=False, ) # print(p.stderr) return p, p.stdout, p.stderr -@pytest.helpers.register def first_page_dimensions(pdf): - from ocrmypdf import pdfinfo - info = pdfinfo.PdfInfo(pdf) page0 = info[0] return (page0.width_inches, page0.height_inches) diff --git a/tests/spoof/gs_feature_elision.py b/tests/plugins/gs_feature_elision.py old mode 100755 new mode 100644 similarity index 56% rename from tests/spoof/gs_feature_elision.py rename to tests/plugins/gs_feature_elision.py index ad65a619..97a17ab3 --- a/tests/spoof/gs_feature_elision.py +++ b/tests/plugins/gs_feature_elision.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -20,40 +19,34 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from unittest.mock import patch -import os -import sys -from subprocess import check_call - - -"""Replicate one type of Ghostscript feature elision warning during -PDF/A creation.""" - - -def real_ghostscript(argv): - gs_args = ['gs'] + argv[1:] - os.execvp("gs", gs_args) - return # Not reachable - +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins import ghostscript +from ocrmypdf.subprocess import run_polling_stderr elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1 not permitted in PDF/A-2, overprint mode not set""" -def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] - if '--version' in sys.argv: - print('9.20') - print('SPOOFED: ' + os.path.basename(__file__)) - sys.exit(0) - gs_args = ['gs'] + sys.argv[1:] - check_call(gs_args) - - if '-sDEVICE=pdfwrite' in sys.argv[1:]: - print(elision_warning) - - sys.exit(0) +def run_append_stderr(*args, **kwargs): + proc = run_polling_stderr(*args, **kwargs) + proc.stderr += '\n' + elision_warning + '\n' + return proc -if __name__ == '__main__': - main() +@hookimpl +def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdfa_part): + with patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as mock: + mock.side_effect = run_append_stderr + ghostscript.generate_pdfa( + pdf_pages=pdf_pages, + pdfmark=pdfmark, + output_file=output_file, + compression=compression, + pdf_version=pdf_version, + pdfa_part=pdfa_part, + progressbar_class=None, + ) + mock.assert_called_once() + return output_file diff --git a/tests/spoof/gs_pdfa_failure.py b/tests/plugins/gs_pdfa_failure.py old mode 100755 new mode 100644 similarity index 56% rename from tests/spoof/gs_pdfa_failure.py rename to tests/plugins/gs_pdfa_failure.py index 730fa5b5..43f6df0f --- a/tests/spoof/gs_pdfa_failure.py +++ b/tests/plugins/gs_pdfa_failure.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -20,46 +19,36 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import os -import sys +from unittest.mock import patch + +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins import ghostscript +from ocrmypdf.subprocess import run_polling_stderr -"""Replicate Ghostscript PDF/A conversion failure by suppressing some -arguments""" - - -def real_ghostscript(argv): - gs_args = ['gs'] + argv[1:] - os.execvp("gs", gs_args) - return # Not reachable - - -def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] - if '--version' in sys.argv: - print('9.20') - print('SPOOFED: ' + os.path.basename(__file__)) - sys.exit(0) - - # Unless some argument is calling for PDFA generation, forward to - # real ghostscript - if not any(arg.startswith('-dPDFA') for arg in sys.argv): - real_ghostscript(sys.argv) - return - +def run_rig_args(args, **kwargs): # Remove the two arguments that tell ghostscript to create a PDF/A # Does not remove the Postscript definition file - not necessary # to cause PDF/A creation failure - argv = [] - for arg in sys.argv: - if arg.startswith('-dPDFA'): - continue - elif arg.startswith('-dPDFACompatibilityPolicy'): - continue - argv.append(arg) - - real_ghostscript(argv) + new_args = [ + arg for arg in args if not arg.startswith('-dPDFA') and not arg.endswith('.ps') + ] + proc = run_polling_stderr(new_args, **kwargs) + return proc -if __name__ == '__main__': - main() +@hookimpl +def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdfa_part): + with patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as mock: + mock.side_effect = run_rig_args + ghostscript.generate_pdfa( + pdf_pages=pdf_pages, + pdfmark=pdfmark, + output_file=output_file, + compression=compression, + pdf_version=pdf_version, + pdfa_part=pdfa_part, + progressbar_class=None, + ) + mock.assert_called() + return output_file diff --git a/tests/spoof/gs_raster_failure.py b/tests/plugins/gs_raster_failure.py old mode 100755 new mode 100644 similarity index 50% rename from tests/spoof/gs_raster_failure.py rename to tests/plugins/gs_raster_failure.py index b404cca8..0d85b263 --- a/tests/spoof/gs_raster_failure.py +++ b/tests/plugins/gs_raster_failure.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -20,35 +19,42 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from pathlib import Path +from subprocess import CalledProcessError +from unittest.mock import patch -import os -import sys - -"""Replicate Ghostscript raster failure while allowing rendering""" +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins import ghostscript -def real_ghostscript(argv): - gs_args = ['gs'] + argv[1:] - os.execvp("gs", gs_args) - return # Not reachable +def raise_gs_fail(*args, **kwargs): + raise CalledProcessError( + 1, 'gs', output=b"", stderr=b"ERROR: Ghost story archive not found" + ) -def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] - if '--version' in sys.argv: - print('9.20') - print('SPOOFED: ' + os.path.basename(__file__)) - sys.exit(0) - - # For any rendering calls (device == pdfwrite) call real ghostscript - if '-sDEVICE=pdfwrite' in sys.argv: - real_ghostscript(sys.argv) - return - - # Fail - print("ERROR: Ghost story archive not found") - sys.exit(1) - - -if __name__ == '__main__': - main() +@hookimpl +def rasterize_pdf_page( + input_file, + output_file, + raster_device, + raster_dpi, + pageno, + page_dpi=None, + rotation=None, + filter_vector=False, +) -> Path: + with patch('ocrmypdf._exec.ghostscript.run') as mock: + mock.side_effect = raise_gs_fail + ghostscript.rasterize_pdf_page( + input_file=input_file, + output_file=output_file, + raster_device=raster_device, + raster_dpi=raster_dpi, + pageno=pageno, + page_dpi=page_dpi, + rotation=rotation, + filter_vector=filter_vector, + ) + mock.assert_called() + return output_file diff --git a/tests/spoof/gs_render_failure.py b/tests/plugins/gs_render_failure.py old mode 100755 new mode 100644 similarity index 54% rename from tests/spoof/gs_render_failure.py rename to tests/plugins/gs_render_failure.py index 3027a684..3c88e71f --- a/tests/spoof/gs_render_failure.py +++ b/tests/plugins/gs_render_failure.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016-18 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -20,34 +19,31 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Replicate Ghostscript render failure while allowing rasterizing""" +from subprocess import CalledProcessError +from unittest.mock import patch -import os -import sys +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins import ghostscript -def real_ghostscript(argv): - gs_args = ['gs'] + argv[1:] - os.execvp("gs", gs_args) - return # Not reachable +def raise_gs_fail(*args, **kwargs): + raise CalledProcessError( + 1, 'gs', output=b"", stderr=b"ERROR: Casper is not a friendly ghost" + ) -def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] - if '--version' in sys.argv: - print('9.20') - print('SPOOFED: ' + os.path.basename(__file__)) - sys.exit(0) - - # For any rasterize calls (device != pdfwrite) call real ghostscript - if '-sDEVICE=pdfwrite' not in sys.argv: - real_ghostscript(sys.argv) - return - - # Fail - print("ERROR: Casper is not a friendly ghost") - sys.exit(1) - - -if __name__ == '__main__': - main() +@hookimpl +def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdfa_part): + with patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as mock: + mock.side_effect = raise_gs_fail + ghostscript.generate_pdfa( + pdf_pages=pdf_pages, + pdfmark=pdfmark, + output_file=output_file, + compression=compression, + pdf_version=pdf_version, + pdfa_part=pdfa_part, + progressbar_class=None, + ) + mock.assert_called() + return output_file diff --git a/tests/plugins/tesseract_badutf8.py b/tests/plugins/tesseract_badutf8.py new file mode 100644 index 00000000..5a5e1e32 --- /dev/null +++ b/tests/plugins/tesseract_badutf8.py @@ -0,0 +1,72 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Tesseract bad utf8 + +In some cases, some versions of Tesseract can output binary gibberish or data +that is not UTF-8 compatible, so we are forced to check that we can convert it +and present it to the user. +""" + +from contextlib import contextmanager +from subprocess import CalledProcessError +from unittest.mock import patch + +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine + + +def bad_utf8(*args, **kwargs): + raise CalledProcessError( + 1, + 'tesseract', + output=b'\x96\xb3\x8c\xf8\x82\xc8UTF-8\x0a', # "Invalid UTF-8" in Shift JIS + stderr=b"", + ) + + +@contextmanager +def patch_tesseract_run(): + with patch('ocrmypdf._exec.tesseract.run') as mock: + mock.side_effect = bad_utf8 + yield + mock.assert_called() + + +class BadUtf8OcrEngine(TesseractOcrEngine): + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_hocr( + input_file, output_hocr, output_text, options + ) + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_pdf( + input_file, output_pdf, output_text, options + ) + + +@hookimpl +def get_ocr_engine(): + return BadUtf8OcrEngine() diff --git a/tests/plugins/tesseract_big_image_error.py b/tests/plugins/tesseract_big_image_error.py new file mode 100644 index 00000000..e2382ac5 --- /dev/null +++ b/tests/plugins/tesseract_big_image_error.py @@ -0,0 +1,70 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from contextlib import contextmanager +from subprocess import CalledProcessError +from unittest.mock import patch + +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine + + +def raise_size_exception(*args, **kwargs): + raise CalledProcessError( + 1, + 'tesseract', + output=b"Image too large: (33830, 14959)\nError during processing.", + stderr=b"", + ) + + +@contextmanager +def patch_tesseract_run(): + with patch('ocrmypdf._exec.tesseract.run') as mock: + mock.side_effect = raise_size_exception + yield + mock.assert_called() + + +class BigImageErrorOcrEngine(TesseractOcrEngine): + @staticmethod + def get_orientation(input_file, options): + with patch_tesseract_run(): + return TesseractOcrEngine.get_orientation(input_file, options) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_hocr( + input_file, output_hocr, output_text, options + ) + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_pdf( + input_file, output_pdf, output_text, options + ) + + +@hookimpl +def get_ocr_engine(): + return BigImageErrorOcrEngine() diff --git a/tests/spoof/tesseract_cache.py b/tests/plugins/tesseract_cache.py old mode 100755 new mode 100644 similarity index 54% rename from tests/spoof/tesseract_cache.py rename to tests/plugins/tesseract_cache.py index db06aa8e..37c4a690 --- a/tests/spoof/tesseract_cache.py +++ b/tests/plugins/tesseract_cache.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -22,11 +21,10 @@ """Cache output of tesseract to speed up test suite -The cache is keyed by an environment variable that slips the input test file -from tests/resources/ to us. The input arguments are slugged into a hideous -filename that more or less represents them literally. Joined together, this -becomes the name of the cache folder. A few name files like stdout, stderr, -hocr, pdf, describe the output to reproduce. +The cache is keyed by by the input test file The input arguments are slugged +into a hideous filename that more or less represents them literally. Joined +together, this becomes the name of the cache folder. A few name files like +stdout, stderr, hocr, pdf, describe the output to reproduce. Changes to tests/resources/ or image processing algorithms don't trigger a cache miss. By design, an input image that varies according to platform @@ -40,10 +38,7 @@ information about the system that produced the results used when cache was generated. This mainly a log to answer questions about how the files were produced. -For performance reasons, especially the slow performance of Tesseract on -machines with AVX2, the cache is now bundled. - -Certain operations are not cached and routed to tesseract directly. +Certain operations are not cached and routed to Tesseract OCR directly. Assumes Tesseract 4.0.0-alpha or higher. @@ -51,20 +46,23 @@ Assumes Tesseract 4.0.0-alpha or higher. import argparse import json -import os +import logging import platform import re import shutil -import subprocess -import sys +from functools import partial from pathlib import Path +from subprocess import PIPE, CalledProcessError, CompletedProcess +from unittest.mock import patch -if '_OCRMYPDF_SAVE_PATH' in os.environ: - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine +from ocrmypdf.subprocess import run -__version__ = subprocess.check_output( - ['tesseract', '--version'], stderr=subprocess.STDOUT -).decode() +log = logging.getLogger(__name__) + +TESTS_ROOT = Path(__file__).resolve().parent.parent +CACHE_ROOT = TESTS_ROOT / 'cache' parser = argparse.ArgumentParser( @@ -80,41 +78,15 @@ parser.add_argument('-c', action='append') parser.add_argument('--psm', type=int) parser.add_argument('--oem', type=int) -TESTS_ROOT = Path(__file__).resolve().parent.parent -CACHE_ROOT = TESTS_ROOT / 'cache' - - -def real_tesseract(): - tess_args = ['tesseract'] + sys.argv[1:] - os.execvp("tesseract", tess_args) - return # Not reachable - - -def main(): - if any( - opt in sys.argv[1:] - for opt in ('--print-parameters', '--list-langs', '--version') - ): - real_tesseract() # jump into real tesseract, replacing this process - - # Convert non-standard but supported -psm to --psm - sys.argv = ['--psm' if arg == '-psm' else arg for arg in sys.argv] - - source = os.environ['_OCRMYPDF_TEST_INFILE'] # required - args = parser.parse_args() - - cache_disabled = os.environ.get('_OCRMYPDF_CACHE_DISABLED', False) - - if args.imagename == 'stdin': - real_tesseract() +def get_cache_folder(source_pdf, run_args, parsed_args): def slugs(): yield '' # so we don't start with a '-' which makes rm difficult - for arg in sys.argv[1:]: - if arg == args.imagename: - yield Path(args.imagename).name - elif arg == args.outputbase: - yield Path(args.outputbase).name + for arg in run_args[1:]: + if arg == parsed_args.imagename: + yield Path(parsed_args.imagename).name + elif arg == parsed_args.outputbase: + yield Path(parsed_args.outputbase).name elif arg == '-c' or arg.startswith('textonly'): pass else: @@ -123,18 +95,26 @@ def main(): argv_slug = '__'.join(slugs()) argv_slug = argv_slug.replace('/', '___') - cache_folder = Path(CACHE_ROOT) / Path(source).stem / argv_slug + return Path(CACHE_ROOT) / Path(source_pdf).stem / argv_slug + + +def cached_run(options, run_args, **run_kwargs): + run_args = [str(arg) for arg in run_args] # flatten PosixPaths + args = parser.parse_args(run_args[1:]) + + if args.imagename in ('stdin', '-'): + return run(run_args, **run_kwargs) + + source_file = options.input_file + cache_folder = get_cache_folder(source_file, run_args, args) cache_folder.mkdir(parents=True, exist_ok=True) - print(f"Tesseract cache folder {cache_folder} - ", end='', file=sys.stderr) + log.debug(f"Using Tesseract cache {cache_folder}") - if (cache_folder / 'stderr.bin').exists() and not cache_disabled: - # Cache hit - print("HIT", file=sys.stderr) + if (cache_folder / 'stderr.bin').exists(): + log.debug("Cache HIT") # Replicate stdout/err - sys.stdout.buffer.write((cache_folder / 'stdout.bin').read_bytes()) - sys.stderr.buffer.write((cache_folder / 'stderr.bin').read_bytes()) if args.outputbase != 'stdout': if not args.configfiles: args.configfiles.append('txt') @@ -142,25 +122,28 @@ def main(): # cp cache -> output tessfile = args.outputbase + '.' + configfile shutil.copy(str(cache_folder / configfile) + '.bin', tessfile) - sys.exit(0) + return CompletedProcess( + args=run_args, + returncode=0, + stdout=(cache_folder / 'stdout.bin').read_bytes(), + stderr=(cache_folder / 'stderr.bin').read_bytes(), + ) - # Cache miss - print("MISS", file=sys.stderr) + log.debug("Cache MISS") - # Call tesseract - print(sys.argv[1:]) - p = subprocess.run( - ['tesseract'] + sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - sys.stdout.buffer.write(p.stdout) - sys.stderr.buffer.write(p.stderr) - - if p.returncode != 0: - # Do not cache errors or crashes - print("Tesseract error", file=sys.stderr) - return p.returncode + cache_kwargs = { + k: v for k, v in run_kwargs.items() if k not in ('stdout', 'stderr') + } + assert cache_kwargs['check'] + try: + p = run(run_args, stdout=PIPE, stderr=PIPE, **cache_kwargs) + except CalledProcessError as e: + log.exception(e) + raise # Pass exception onward + # Update cache (cache_folder / 'stdout.bin').write_bytes(p.stdout) + (cache_folder / 'stderr.bin').write_bytes(p.stderr) if args.outputbase != 'stdout': if not args.configfiles: @@ -173,27 +156,46 @@ def main(): tessfile = args.outputbase + '.' + configfile shutil.copy(tessfile, str(cache_folder / configfile) + '.bin') - (cache_folder / 'stderr.bin').write_bytes(p.stderr) - manifest = {} - manifest['tesseract_version'] = __version__.replace('\n', ' ') + manifest['tesseract_version'] = TesseractOcrEngine.version().replace('\n', ' ') manifest['platform'] = platform.platform() manifest['python'] = platform.python_version() - manifest['argv_slug'] = argv_slug - manifest['sourcefile'] = str(Path(source).relative_to(TESTS_ROOT)) + manifest['argv_slug'] = cache_folder.name + manifest['sourcefile'] = str(Path(source_file).relative_to(TESTS_ROOT)) def clean_sys_argv(): - for arg in sys.argv[1:]: - yield re.sub(r'.*/com.github.ocrmypdf[^/]+[/](.*)', r'$TMPDIR/\1', arg) + for arg in run_args[1:]: + yield re.sub(r'.*/ocrmypdf[.]io[.][^/]+[/](.*)', r'$TMPDIR/\1', arg) manifest['args'] = list(clean_sys_argv()) - - # pylint: disable=E1101 with (Path(CACHE_ROOT) / 'manifest.jsonl').open('a') as f: json.dump(manifest, f) f.write('\n') f.flush() + return p -if __name__ == '__main__': - main() +class CacheOcrEngine(TesseractOcrEngine): + @staticmethod + def get_orientation(input_file, options): + with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)): + return TesseractOcrEngine.get_orientation(input_file, options) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)): + TesseractOcrEngine.generate_hocr( + input_file, output_hocr, output_text, options + ) + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)): + TesseractOcrEngine.generate_pdf( + input_file, output_pdf, output_text, options + ) + + +@hookimpl +def get_ocr_engine(): + return CacheOcrEngine() diff --git a/tests/plugins/tesseract_crash.py b/tests/plugins/tesseract_crash.py new file mode 100755 index 00000000..60ff1110 --- /dev/null +++ b/tests/plugins/tesseract_crash.py @@ -0,0 +1,72 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import signal +from contextlib import contextmanager +from subprocess import CalledProcessError +from unittest.mock import patch + +from ocrmypdf import hookimpl +from ocrmypdf.builtin_plugins.tesseract_ocr import TesseractOcrEngine + + +def raise_crash(*args, **kwargs): + raise CalledProcessError( + 128 + signal.SIGABRT, + 'tesseract', + output=b"", + stderr=b"libc++abi.dylib: terminating with uncaught exception of type " + + b"std::bad_alloc: std::bad_alloc", + ) + + +@contextmanager +def patch_tesseract_run(): + with patch('ocrmypdf._exec.tesseract.run') as mock: + mock.side_effect = raise_crash + yield + mock.assert_called() + + +class CrashOcrEngine(TesseractOcrEngine): + @staticmethod + def get_orientation(input_file, options): + with patch_tesseract_run(): + return TesseractOcrEngine.get_orientation(input_file, options) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_hocr( + input_file, output_hocr, output_text, options + ) + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with patch_tesseract_run(): + TesseractOcrEngine.generate_pdf( + input_file, output_pdf, output_text, options + ) + + +@hookimpl +def get_ocr_engine(): + return CrashOcrEngine() diff --git a/tests/plugins/tesseract_debug_rotate.py b/tests/plugins/tesseract_debug_rotate.py new file mode 100644 index 00000000..30c613bd --- /dev/null +++ b/tests/plugins/tesseract_debug_rotate.py @@ -0,0 +1,112 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Tesseract no-op/fixed rotate plugin + +To quickly run tests where getting OCR output is not necessary and we want to test +the rotation pipeline. + +In 'hocr' mode, create a .hocr file that specifies no text found. + +In 'pdf' mode, convert the image to PDF using another program. + +In orientation check mode, report 0, 90, 180, 270... based on page number. +""" + +import pikepdf +from PIL import Image + +from ocrmypdf import OcrEngine, OrientationConfidence, hookimpl +from ocrmypdf.helpers import page_number + +HOCR_TEMPLATE = ''' + + + + + + + + + +
+
+

+ + +

+
+
+ +''' + + +class FixedRotateNoopOcrEngine(OcrEngine): + @staticmethod + def version(): + return '4.0.0' + + @staticmethod + def creator_tag(options): + tag = '-PDF' if options.pdf_renderer == 'sandwich' else '' + return f"NO-OP {tag} {FixedRotateNoopOcrEngine.version()}" + + def __str__(self): + return f"NO-OP {FixedRotateNoopOcrEngine.version()}" + + @staticmethod + def languages(options): + return {'eng'} + + @staticmethod + def get_orientation(input_file, options): + page = page_number(input_file) + + angle = ((page - 1) * 90) % 360 + + return OrientationConfidence(angle=angle, confidence=99.9) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with Image.open(input_file) as im, open( + output_hocr, 'w', encoding='utf-8' + ) as f: + w, h = im.size + f.write(HOCR_TEMPLATE.format(str(w), str(h))) + with open(output_text, 'w') as f: + f.write('') + + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with Image.open(input_file) as im: + dpi = im.info['dpi'] + pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1] + ptsize = pagesize[0] * 72, pagesize[1] * 72 + pdf = pikepdf.new() + pdf.add_blank_page(page_size=ptsize) + pdf.save(output_pdf, static_id=True) + output_text.write_text('') + + +@hookimpl +def get_ocr_engine(): + return FixedRotateNoopOcrEngine() diff --git a/tests/spoof/tesseract_noop.py b/tests/plugins/tesseract_noop.py old mode 100755 new mode 100644 similarity index 50% rename from tests/spoof/tesseract_noop.py rename to tests/plugins/tesseract_noop.py index cdb71664..26bfe1df --- a/tests/spoof/tesseract_noop.py +++ b/tests/plugins/tesseract_noop.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 +# © 2020 James R. Barlow: github.com/jbarlow83 # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the @@ -20,7 +19,7 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Tesseract no-op spoof +"""Tesseract no-op plugin To quickly run tests where getting OCR output is not necessary. @@ -31,20 +30,10 @@ In 'pdf' mode, convert the image to PDF using another program. In orientation check mode, report the orientation is upright. """ -import sys - -import img2pdf -import PyPDF2 as pypdf +import pikepdf from PIL import Image -VERSION_STRING = '''tesseract 4.0.0 - leptonica-1.77.0 - libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 - Found AVX2 - Found AVX - Found SSE -SPOOFED -''' +from ocrmypdf import OcrEngine, OrientationConfidence, hookimpl HOCR_TEMPLATE = ''' ''' -def main(): - if sys.argv[1] == '--version': - print(VERSION_STRING, file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--list-langs': - print('List of available languages (1):\neng', file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--print-parameters': - print("Some parameters", file=sys.stderr) - print("textonly_pdf\t1\tSome help text") - sys.exit(0) - elif sys.argv[-2] == 'hocr': - inputf = sys.argv[-4] - output = sys.argv[-3] - with Image.open(inputf) as im, open( - output + '.hocr', 'w', encoding='utf-8' +class NoopOcrEngine(OcrEngine): + @staticmethod + def version(): + return '4.0.0' + + @staticmethod + def creator_tag(options): + tag = '-PDF' if options.pdf_renderer == 'sandwich' else '' + return f"NO-OP {tag} {NoopOcrEngine.version()}" + + def __str__(self): + return f"NO-OP {NoopOcrEngine.version()}" + + @staticmethod + def languages(options): + return {'eng'} + + @staticmethod + def get_orientation(input_file, options): + return OrientationConfidence(angle=0, confidence=0.0) + + @staticmethod + def generate_hocr(input_file, output_hocr, output_text, options): + with Image.open(input_file) as im, open( + output_hocr, 'w', encoding='utf-8' ) as f: w, h = im.size f.write(HOCR_TEMPLATE.format(str(w), str(h))) - with open(output + '.txt', 'w') as f: + with open(output_text, 'w') as f: f.write('') - elif sys.argv[-2] == 'pdf': - if 'textonly_pdf=1' in sys.argv: - inputf = sys.argv[-4] - output = sys.argv[-3] - with Image.open(inputf) as im: - dpi = im.info['dpi'] - pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1] - ptsize = pagesize[0] * 72, pagesize[1] * 72 - pdf_out = pypdf.PdfFileWriter() - pdf_out.addBlankPage(ptsize[0], ptsize[1]) - with open(output + '.pdf', 'wb') as f: - pdf_out.write(f) - with open(output + '.txt', 'w') as f: - f.write('') - else: - inputf = sys.argv[-4] - output = sys.argv[-3] - pdf_bytes = img2pdf.convert([inputf], dpi=300) - with open(output + '.pdf', 'wb') as f: - f.write(pdf_bytes) - with open(output + '.txt', 'w') as f: - f.write('') - elif sys.argv[-1] == 'stdout': - inputf = sys.argv[-2] - print( - """Orientation: 0 -Orientation in degrees: 0 -Orientation confidence: 100.00 -Script: 1 -Script confidence: 100.00""", - file=sys.stderr, - ) - else: - print("Spoof doesn't understand arguments", file=sys.stderr) - print(sys.argv, file=sys.stderr) - sys.exit(1) - - sys.exit(0) + @staticmethod + def generate_pdf(input_file, output_pdf, output_text, options): + with Image.open(input_file) as im: + dpi = im.info['dpi'] + pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1] + ptsize = pagesize[0] * 72, pagesize[1] * 72 + pdf = pikepdf.new() + pdf.add_blank_page(page_size=ptsize) + pdf.save(output_pdf, static_id=True) + output_text.write_text('') -if __name__ == '__main__': - main() +@hookimpl +def get_ocr_engine(): + return NoopOcrEngine() diff --git a/tests/resources/3small.pdf b/tests/resources/3small.pdf new file mode 100644 index 00000000..7d282496 Binary files /dev/null and b/tests/resources/3small.pdf differ diff --git a/tests/resources/acroform.pdf b/tests/resources/acroform.pdf new file mode 100644 index 00000000..b80eb44a Binary files /dev/null and b/tests/resources/acroform.pdf differ diff --git a/tests/resources/baiona_cmyk.jpg b/tests/resources/baiona_cmyk.jpg new file mode 100644 index 00000000..01d6badf Binary files /dev/null and b/tests/resources/baiona_cmyk.jpg differ diff --git a/tests/resources/enron1.pdf b/tests/resources/enron1.pdf deleted file mode 100644 index 65aa7fb6..00000000 Binary files a/tests/resources/enron1.pdf and /dev/null differ diff --git a/tests/spoof/tesseract_badutf8.py b/tests/spoof/tesseract_badutf8.py deleted file mode 100755 index ba21bb17..00000000 --- a/tests/spoof/tesseract_badutf8.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# © 2017 James R. Barlow: github.com/jbarlow83 -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import sys - - -"""Tesseract bad utf8 spoof - -In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode -text because tesseract seems to do that in some cases related to -language pack version mismatches - -""" - - -VERSION_STRING = '''tesseract 4.0.0 - leptonica-1.77.0 - libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 - Found AVX2 - Found AVX - Found SSE -SPOOFED -''' - -# Japanese "Invalid UTF-8" encoded in Shift JIS -BAD_UTF8 = b'\x96\xb3\x8c\xf8\x82\xc8UTF-8\x0a' - - -def main(): - if sys.argv[1] == '--version': - print(VERSION_STRING, file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--list-langs': - print('List of available languages (1):\neng', file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--print-parameters': - print("Some parameters", file=sys.stderr) - print("textonly_pdf\t1\tSome help text") - sys.exit(0) - elif sys.argv[-2] in ('hocr', 'pdf'): - sys.stdout.buffer.write(BAD_UTF8) - sys.exit(1) - elif sys.argv[-1] == 'stdout': - # input file is at sys.argv[-2] but we don't look at it - print( - """Orientation: 0 -Orientation in degrees: 0 -Orientation confidence: 100.00 -Script: 1 -Script confidence: 100.00""", - file=sys.stderr, - ) - else: - print("Spoof doesn't understand arguments", file=sys.stderr) - print(sys.argv, file=sys.stderr) - sys.exit(1) - - sys.exit(0) - - -if __name__ == '__main__': - main() diff --git a/tests/spoof/tesseract_big_image_error.py b/tests/spoof/tesseract_big_image_error.py deleted file mode 100755 index d3ee53d8..00000000 --- a/tests/spoof/tesseract_big_image_error.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import sys - -VERSION_STRING = '''tesseract 4.0.0 - leptonica-1.77.0 - libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 - Found AVX2 - Found AVX - Found SSE -SPOOFED: return error claiming image too big -''' - -"""Simulates an error of Tesseract failing on attempts to process large images - -""" - - -def main(): - if sys.argv[1] == '--version': - print(VERSION_STRING, file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--list-langs': - print('List of available languages (1):\neng\n', file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--print-parameters': - print('A parameter list would go here\ntextonly_pdf 0\n', file=sys.stderr) - sys.exit(0) - elif sys.argv[-2] == 'hocr': - print( - "Image too large: (33830, 14959)\n" "Error during processing.", - file=sys.stderr, - ) - sys.exit(1) - elif sys.argv[-2] == 'pdf': - print( - "Image too large: (33830, 14959)\n" "Error during processing.", - file=sys.stderr, - ) - sys.exit(1) - elif sys.argv[-1] == 'stdout': - print( - "Image too large: (33830, 14959)\n" "Error during processing.", - file=sys.stderr, - ) - sys.exit(1) - else: - print("Spoof doesn't understand arguments", file=sys.stderr) - print(sys.argv, file=sys.stderr) - sys.exit(1) - - sys.exit(0) - - -if __name__ == '__main__': - main() diff --git a/tests/spoof/tesseract_crash.py b/tests/spoof/tesseract_crash.py deleted file mode 100755 index 8b6f90d8..00000000 --- a/tests/spoof/tesseract_crash.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import signal -import sys - -VERSION_STRING = '''tesseract 4.0.0 - leptonica-1.77.0 - libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0 - Found AVX2 - Found AVX - Found SSE -SPOOFED: CRASH ON OCR or --psm 0 -''' - -"""Simulates a Tesseract crash when asked to run OCR - -It isn't strictly necessary to crash the process and that has unwanted -side effects like triggering core dumps or error reporting, logging and such. -It's enough to dump some text to stderr and return an error code. - -Follows the POSIX(?) convention of returning 128 + signal number. - -""" - - -def main(): - if sys.argv[1] == '--version': - print(VERSION_STRING, file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--list-langs': - print('List of available languages (1):\neng', file=sys.stderr) - sys.exit(0) - elif sys.argv[1] == '--print-parameters': - print('A parameter list would go here\ntextonly_pdf 0\n', file=sys.stderr) - sys.exit(0) - elif sys.argv[-2] == 'hocr': - print("KABOOM! Tesseract failed for some reason", file=sys.stderr) - sys.exit(128 + signal.SIGSEGV) - elif sys.argv[-2] == 'pdf': - print("KABOOM! Tesseract failed for some reason", file=sys.stderr) - sys.exit(128 + signal.SIGSEGV) - elif sys.argv[-1] == 'stdout': - print( - "libc++abi.dylib: terminating with uncaught exception of type " - "std::bad_alloc: std::bad_alloc", - file=sys.stderr, - ) - sys.exit(128 + signal.SIGABRT) - else: - print("Spoof doesn't understand arguments", file=sys.stderr) - print(sys.argv, file=sys.stderr) - sys.exit(1) - - sys.exit(0) - - -if __name__ == '__main__': - main() diff --git a/tests/spoof/unpaper_oldversion.py b/tests/spoof/unpaper_oldversion.py deleted file mode 100755 index ff2e27ea..00000000 --- a/tests/spoof/unpaper_oldversion.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# © 2016 James R. Barlow: github.com/jbarlow83 -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import sys - - -def main(): - if sys.argv[1] == '--version': - print('0.5') - sys.exit(0) - - print("Only supports --version") - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/tests/test_acroform.py b/tests/test_acroform.py new file mode 100644 index 00000000..82fe8d1f --- /dev/null +++ b/tests/test_acroform.py @@ -0,0 +1,34 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging + +import pytest + +import ocrmypdf + +from .conftest import check_ocrmypdf + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def acroform(resources): + return resources / 'acroform.pdf' + + +def test_acroform_and_redo(acroform, caplog, no_outpdf): + with pytest.raises(ocrmypdf.exceptions.InputFileError): + check_ocrmypdf(acroform, no_outpdf, '--redo-ocr') + assert '--redo-ocr is not currently possible' in caplog.text + + +def test_acroform_message(acroform, caplog, outpdf): + caplog.set_level(logging.INFO) + check_ocrmypdf(acroform, outpdf, '--plugin', 'tests/plugins/tesseract_noop.py') + assert 'fillable form' in caplog.text + assert '--force-ocr' in caplog.text diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..78323ce0 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,67 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +from io import BytesIO, StringIO + +import pytest +from tqdm import tqdm + +import ocrmypdf + + +def test_raw_console(): + bio = StringIO() + tqconsole = ocrmypdf.api.TqdmConsole(file=bio) + tqconsole.write("Test") + tqconsole.flush() + assert "Test" in bio.getvalue() + + +def test_tqdm_console(): + log = logging.getLogger() + log.setLevel(logging.INFO) + + formatter = logging.Formatter('%(message)s') + + bio = StringIO() + console = logging.StreamHandler(ocrmypdf.api.TqdmConsole(file=bio)) + console.setFormatter(formatter) + + log.addHandler(console) + + def before_pbar(message): + # Ensure that log messages appear before the progress bar, even when + # printed after the progress bar updates. + v = bio.getvalue() + pbar_start_marker = '|#' + return v.index(message) < v.index(pbar_start_marker) + + with tqdm(total=2, file=bio, disable=False) as pbar: + pbar.update() + msg = "1/2 above progress bar" + log.info(msg) + assert before_pbar(msg) + + log.info("done") + assert not before_pbar("done") + + +def test_language_list(): + with pytest.raises( + (ocrmypdf.exceptions.InputFileError, ocrmypdf.exceptions.MissingDependencyError) + ): + ocrmypdf.ocr('doesnotexist.pdf', '_.pdf', language=['eng', 'deu']) + + +def test_stream_api(resources): + in_ = (resources / 'graph.pdf').open('rb') + out = BytesIO() + + ocrmypdf.ocr(in_, out, tesseract_timeout=0.0) + out.seek(0) + assert b'%PDF' in out.read(1024) diff --git a/tests/test_check_pdf.py b/tests/test_check_pdf.py new file mode 100644 index 00000000..2d192dac --- /dev/null +++ b/tests/test_check_pdf.py @@ -0,0 +1,15 @@ +# © 2018 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import pytest + +from ocrmypdf.helpers import check_pdf + + +def test_pdf_error(resources): + assert check_pdf(resources / 'blank.pdf') + assert not check_pdf(__file__) diff --git a/tests/test_completion.py b/tests/test_completion.py new file mode 100644 index 00000000..20d716ff --- /dev/null +++ b/tests/test_completion.py @@ -0,0 +1,49 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os +from subprocess import PIPE, run + +import pytest + +from .conftest import running_in_docker + +pytestmark = pytest.mark.skipif( + running_in_docker(), + reason="docker can't complete", +) + + +def test_fish(): + try: + proc = run( + ['fish', '-n', 'misc/completion/ocrmypdf.fish'], + check=True, + encoding='utf-8', + stdout=PIPE, + stderr=PIPE, + ) + assert proc.stderr == '', proc.stderr + except FileNotFoundError: + pytest.xfail('fish is not installed') + + +@pytest.mark.skipif( + os.name == 'nt', reason="Windows CI workers have bash but are best left alone" +) +def test_bash(): + try: + proc = run( + ['bash', '-n', 'misc/completion/ocrmypdf.bash'], + check=True, + encoding='utf-8', + stdout=PIPE, + stderr=PIPE, + ) + assert proc.stderr == '', proc.stderr + except FileNotFoundError: + pytest.xfail('bash is not installed') diff --git a/tests/test_ghostscript.py b/tests/test_ghostscript.py index 8756e8a8..0907b819 100644 --- a/tests/test_ghostscript.py +++ b/tests/test_ghostscript.py @@ -1,19 +1,9 @@ # © 2019 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + import logging from decimal import Decimal @@ -22,60 +12,115 @@ import pikepdf import pytest from PIL import Image -from ocrmypdf.exec.ghostscript import rasterize_pdf +from ocrmypdf._exec.ghostscript import rasterize_pdf +from ocrmypdf.exceptions import ExitCode +from ocrmypdf.helpers import Resolution + +from .conftest import check_ocrmypdf, run_ocrmypdf + +# pylint: disable=redefined-outer-name @pytest.fixture -def linn(resources): - path = resources / 'linn.pdf' +def francais(resources): + path = resources / 'francais.pdf' return path, pikepdf.open(path) -def test_rasterize_size(linn, outdir, caplog): - path, pdf = linn +def test_rasterize_size(francais, outdir): + path, pdf = francais page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3]) assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0 page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72)) - target_size = Decimal('200.0'), Decimal('150.0') - target_dpi = 42.0, 4242.0 + target_size = Decimal('50.0'), Decimal('30.0') + forced_dpi = Resolution(42.0, 4242.0) - log = logging.getLogger() rasterize_pdf( path, outdir / 'out.png', - target_size[0] / page_size[0], - target_size[1] / page_size[1], raster_device='pngmono', - log=log, - page_dpi=target_dpi, + raster_dpi=Resolution( + target_size[0] / page_size[0], target_size[1] / page_size[1] + ), + page_dpi=forced_dpi, ) with Image.open(outdir / 'out.png') as im: assert im.size == target_size - assert im.info['dpi'] == target_dpi + assert im.info['dpi'] == forced_dpi -def test_rasterize_rotated(linn, outdir, caplog): - path, pdf = linn +def test_rasterize_rotated(francais, outdir, caplog): + path, pdf = francais page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3]) assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0 page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72)) - target_size = Decimal('200.0'), Decimal('150.0') - target_dpi = 42.0, 4242.0 + target_size = Decimal('50.0'), Decimal('30.0') + forced_dpi = Resolution(42.0, 4242.0) - log = logging.getLogger() caplog.set_level(logging.DEBUG) rasterize_pdf( path, outdir / 'out.png', - target_size[0] / page_size[0], - target_size[1] / page_size[1], raster_device='pngmono', - log=log, - page_dpi=target_dpi, + raster_dpi=Resolution( + target_size[0] / page_size[0], target_size[1] / page_size[1] + ), + page_dpi=forced_dpi, rotation=90, ) with Image.open(outdir / 'out.png') as im: assert im.size == (target_size[1], target_size[0]) - assert im.info['dpi'] == (target_dpi[1], target_dpi[0]) + assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0]) + + +def test_gs_render_failure(resources, outpdf): + p, _out, err = run_ocrmypdf( + resources / 'blank.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + '--plugin', + 'tests/plugins/gs_render_failure.py', + ) + assert 'Casper is not a friendly ghost' in err + assert p.returncode == ExitCode.child_process_error + + +def test_gs_raster_failure(resources, outpdf): + p, _out, err = run_ocrmypdf( + resources / 'francais.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + '--plugin', + 'tests/plugins/gs_raster_failure.py', + ) + assert 'Ghost story archive not found' in err + assert p.returncode == ExitCode.child_process_error + + +def test_ghostscript_pdfa_failure(resources, outpdf): + p, _out, _err = run_ocrmypdf( + resources / 'francais.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + '--plugin', + 'tests/plugins/gs_pdfa_failure.py', + ) + assert ( + p.returncode == ExitCode.pdfa_conversion_failed + ), "Unexpected return when PDF/A fails" + + +def test_ghostscript_feature_elision(resources, outpdf): + check_ocrmypdf( + resources / 'francais.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + '--plugin', + 'tests/plugins/gs_feature_elision.py', + ) diff --git a/tests/test_graft.py b/tests/test_graft.py new file mode 100644 index 00000000..19fa3b9d --- /dev/null +++ b/tests/test_graft.py @@ -0,0 +1,43 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from unittest.mock import patch + +import pikepdf +import pytest + +import ocrmypdf + + +def test_no_glyphless_graft(resources, outdir): + with pikepdf.open(resources / 'francais.pdf') as pdf, pikepdf.open( + resources / 'aspect.pdf' + ) as pdf_aspect, pikepdf.open(resources / 'cmyk.pdf') as pdf_cmyk: + pdf.pages.extend(pdf_aspect.pages) + pdf.pages.extend(pdf_cmyk.pages) + pdf.save(outdir / 'test.pdf') + + with patch('ocrmypdf._graft.MAX_REPLACE_PAGES', 2): + ocrmypdf.ocr( + outdir / 'test.pdf', + outdir / 'out.pdf', + deskew=True, + tesseract_timeout=0, + force_ocr=True, + ) + # This test needs asserts + + +def test_links(resources, outpdf): + ocrmypdf.ocr( + resources / 'link.pdf', outpdf, redo_ocr=True, oversample=200, output_type='pdf' + ) + with pikepdf.open(outpdf) as pdf: + p1 = pdf.pages[0] + p2 = pdf.pages[1] + assert p1.Annots[0].A.D[0].objgen == p2.objgen + assert p2.Annots[0].A.D[0].objgen == p1.objgen diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 00000000..5af7f610 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,130 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import multiprocessing +import os +from unittest.mock import MagicMock + +import pytest + +from ocrmypdf import helpers + +from .conftest import running_in_docker + + +class TestSafeSymlink: + def test_safe_symlink_link_self(self, tmp_path, caplog): + helpers.safe_symlink(tmp_path / 'self', tmp_path / 'self') + assert caplog.record_tuples[0][1] == logging.WARNING + + def test_safe_symlink_overwrite(self, tmp_path): + (tmp_path / 'regular_file').touch() + with pytest.raises(FileExistsError): + helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file') + + def test_safe_symlink_relink(self, tmp_path): + (tmp_path / 'regular_file_a').touch() + (tmp_path / 'regular_file_b').write_bytes(b'ABC') + (tmp_path / 'link').symlink_to(tmp_path / 'regular_file_a') + helpers.safe_symlink(tmp_path / 'regular_file_b', tmp_path / 'link') + assert (tmp_path / 'link').samefile(tmp_path / 'regular_file_b') or ( + tmp_path / 'link' + ).read_bytes() == b'ABC' + + +def test_no_cpu_count(monkeypatch): + invoked = False + + def cpu_count_raises(): + nonlocal invoked + invoked = True + raise NotImplementedError() + + monkeypatch.setattr(multiprocessing, 'cpu_count', cpu_count_raises) + with pytest.warns(expected_warning=UserWarning): + assert helpers.available_cpu_count() == 1 + assert invoked, "Patched function called during test" + + +def test_deprecated(): + @helpers.deprecated + def old_function(): + return 42 + + with pytest.deprecated_call(): + assert old_function() == 42 + + +skipif_docker = pytest.mark.skipif(running_in_docker(), reason="fails on Docker") + + +class TestFileIsWritable: + @pytest.fixture + def non_existent(self, tmp_path): + return tmp_path / 'nofile' + + @pytest.fixture + def basic_file(self, tmp_path): + basic = tmp_path / 'basic' + basic.touch() + return basic + + def test_plain(self, non_existent): + assert helpers.is_file_writable(non_existent) + + def test_symlink_loop(self, tmp_path): + loop = tmp_path / 'loop' + loop.symlink_to(loop) + assert not helpers.is_file_writable(loop) + + @skipif_docker + def test_chmod(self, basic_file): + assert helpers.is_file_writable(basic_file) + basic_file.chmod(0o400) + assert not helpers.is_file_writable(basic_file) + basic_file.chmod(0o000) + assert not helpers.is_file_writable(basic_file) + + def test_permission_error(self, basic_file): + pathmock = MagicMock(spec_set=basic_file) + pathmock.is_symlink.return_value = False + pathmock.exists.return_value = True + pathmock.is_file.side_effect = PermissionError + assert not helpers.is_file_writable(pathmock) + + +@pytest.mark.skipif(os.name != 'nt', reason="Windows test") +def test_shim_paths(tmp_path): + # pylint: disable=import-outside-toplevel + from ocrmypdf.subprocess._windows import shim_env_path + + progfiles = tmp_path / 'Program Files' + progfiles.mkdir() + (progfiles / 'tesseract-ocr').mkdir() + (progfiles / 'gs' / '9.51' / 'bin').mkdir(parents=True) + (progfiles / 'gs' / '9.52' / 'bin').mkdir(parents=True) + syspath = tmp_path / 'bin' + env = {'PROGRAMFILES': str(progfiles), 'PATH': str(syspath)} + + result_str = shim_env_path(env=env) + results = result_str.split(os.pathsep) + assert results[0] == str(syspath), results + assert results[-3].endswith('tesseract-ocr'), results + assert results[-2].endswith(os.path.join('gs', '9.52', 'bin')), results + assert results[-1].endswith(os.path.join('gs', '9.51', 'bin')), results + + +def test_resolution(): + Resolution = helpers.Resolution + dpi_100 = Resolution(100, 100) + dpi_200 = Resolution(200, 200) + assert dpi_100.is_square + assert not Resolution(100, 200).is_square + assert dpi_100 == Resolution(100, 100) + assert str(dpi_100) != str(dpi_200) + assert dpi_100.take_max([200, 300], [400]) == Resolution(300, 400) diff --git a/tests/test_hocrtransform.py b/tests/test_hocrtransform.py index e36fe7ee..b0046e94 100644 --- a/tests/test_hocrtransform.py +++ b/tests/test_hocrtransform.py @@ -1,36 +1,49 @@ # © 2015 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. -from pathlib import Path + +import re +from io import StringIO import pytest +from pdfminer.converter import TextConverter +from pdfminer.layout import LAParams +from pdfminer.pdfdocument import PDFDocument +from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager +from pdfminer.pdfpage import PDFPage +from pdfminer.pdfparser import PDFParser from PIL import Image from ocrmypdf import hocrtransform -from ocrmypdf.exec import qpdf -from ocrmypdf.exec.tesseract import HOCR_TEMPLATE +from ocrmypdf._exec.tesseract import HOCR_TEMPLATE +from ocrmypdf.helpers import check_pdf + +from .conftest import check_ocrmypdf + + +def text_from_pdf(filename): + output_string = StringIO() + with open(filename, 'rb') as in_file: + parser = PDFParser(in_file) + doc = PDFDocument(parser) + rsrcmgr = PDFResourceManager() + device = TextConverter(rsrcmgr, output_string, laparams=LAParams()) + interpreter = PDFPageInterpreter(rsrcmgr, device) + for page in PDFPage.create_pages(doc): + interpreter.process_page(page) + return output_string.getvalue() + # pylint: disable=redefined-outer-name @pytest.fixture -def blank_hocr(tmpdir): - filename = Path(str(tmpdir)) / "blank.hocr" - filename.write_text(HOCR_TEMPLATE) # pylint: disable=E1101 +def blank_hocr(tmp_path): + filename = tmp_path / "blank.hocr" + filename.write_text(HOCR_TEMPLATE) return filename @@ -40,7 +53,30 @@ def test_mono_image(blank_hocr, outdir): im.putpixel((n, n), 1) im.save(outdir / 'mono.tif', format='TIFF') - hocr = hocrtransform.HocrTransform(str(blank_hocr), 300) - hocr.to_pdf(str(outdir / 'mono.pdf'), imageFileName=str(outdir / 'mono.tif')) + hocr = hocrtransform.HocrTransform(hocr_filename=str(blank_hocr), dpi=300) + hocr.to_pdf( + out_filename=str(outdir / 'mono.pdf'), image_filename=str(outdir / 'mono.tif') + ) - qpdf.check(str(outdir / 'mono.pdf')) + check_pdf(str(outdir / 'mono.pdf')) + + +@pytest.mark.slow +def test_hocrtransform_matches_sandwich(resources, outdir): + check_ocrmypdf(resources / 'ccitt.pdf', outdir / 'hocr.pdf', '--pdf-renderer=hocr') + check_ocrmypdf( + resources / 'ccitt.pdf', outdir / 'tess.pdf', '--pdf-renderer=sandwich' + ) + + def clean(s): + s = re.sub(r'[ ]+', ' ', s) + s = re.sub(r'[ ]?[\n]+', r'\n', s) + return s + + hocr_txt = clean(text_from_pdf(outdir / 'hocr.pdf')) + tess_txt = clean(text_from_pdf(outdir / 'tess.pdf')) + + # Path('hocr.txt').write_text(hocr_txt) + # Path('tess.txt').write_text(tess_txt) + + assert hocr_txt == tess_txt diff --git a/tests/test_image_input.py b/tests/test_image_input.py new file mode 100644 index 00000000..78ad5fd1 --- /dev/null +++ b/tests/test_image_input.py @@ -0,0 +1,90 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from unittest.mock import patch + +import img2pdf +import pikepdf +import pytest +from PIL import Image + +import ocrmypdf + +from .conftest import check_ocrmypdf, run_ocrmypdf_api + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def baiona(resources): + return Image.open(resources / 'baiona_gray.png') + + +def test_image_to_pdf(resources, outpdf): + check_ocrmypdf( + resources / 'crom.png', + outpdf, + '--image-dpi', + '200', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + + +def test_no_dpi_info(caplog, baiona, outdir, no_outpdf): + im = baiona + assert 'dpi' not in im.info + input_image = outdir / 'baiona_no_dpi.png' + im.save(input_image) + + rc = run_ocrmypdf_api(input_image, no_outpdf) + assert rc == ocrmypdf.ExitCode.input_file + assert "--image-dpi" in caplog.text + + +def test_dpi_not_credible(caplog, baiona, outdir, no_outpdf): + im = baiona + assert 'dpi' not in im.info + input_image = outdir / 'baiona_no_dpi.png' + im.save(input_image, dpi=(30, 30)) + + rc = run_ocrmypdf_api(input_image, no_outpdf) + assert rc == ocrmypdf.ExitCode.input_file + assert "not credible" in caplog.text + + +def test_cmyk_no_icc(caplog, resources, no_outpdf): + rc = run_ocrmypdf_api(resources / 'baiona_cmyk.jpg', no_outpdf) + assert rc == ocrmypdf.ExitCode.input_file + assert "no ICC profile" in caplog.text + + +def test_img2pdf_fails(resources, no_outpdf): + with patch( + 'ocrmypdf._pipeline.img2pdf.convert', side_effect=img2pdf.ImageOpenError() + ) as mock: + rc = run_ocrmypdf_api( + resources / 'baiona_gray.png', no_outpdf, '--image-dpi', '200' + ) + assert rc == ocrmypdf.ExitCode.input_file + mock.assert_called() + + +def test_jpeg_in_jpeg_out(resources, outpdf): + check_ocrmypdf( + resources / 'congress.jpg', + outpdf, + '--image-dpi', + '100', + '--output-type', + 'pdf', # specifically check pdf because Ghostscript may convert to JPEG + '--remove-background', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + with pikepdf.open(outpdf) as pdf: + assert next(pdf.pages[0].images.values()).Filter == pikepdf.Name.DCTDecode diff --git a/tests/test_lept.py b/tests/test_lept.py index 5fe68109..996aa0fe 100644 --- a/tests/test_lept.py +++ b/tests/test_lept.py @@ -1,44 +1,32 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. from os import fspath -import os from pickle import dumps, loads -from unittest.mock import patch import pytest from PIL import Image, ImageChops -import ocrmypdf.leptonica as lept +from ocrmypdf import leptonica as lp def test_colormap_backgroundnorm(resources): # Issue #262 - unclear how to reproduce exactly, so just ensure leptonica # can handle that case - pix = lept.Pix.open(resources / 'baiona_colormapped.png') + pix = lp.Pix.open(resources / 'baiona_colormapped.png') pix.background_norm() @pytest.fixture def crom_pix(resources): - pix = lept.Pix.open(resources / 'crom.png') + pix = lp.Pix.open(resources / 'crom.png') im = Image.open(resources / 'crom.png') - return pix, im + yield pix, im + im.close() def test_pix_basic(crom_pix): @@ -62,14 +50,18 @@ def test_pix_otsu(crom_pix): assert im1bpp.mode == '1' +@pytest.mark.skipif( + lp.get_leptonica_version() < 'leptonica-1.76', + reason="needs new leptonica for API change", +) def test_crop(resources): - pix = lept.Pix.open(resources / 'linn.png') + pix = lp.Pix.open(resources / 'linn.png') foreground = pix.crop_to_foreground() assert foreground.width < pix.width def test_clean_bg(resources): - pix = lept.Pix.open(resources / 'congress.jpg') + pix = lp.Pix.open(resources / 'congress.jpg') imbg = pix.clean_background_to_white() @@ -80,25 +72,25 @@ def test_pickle(crom_pix): assert pix.mode == pix2.mode -def test_leptonica_compile(tmpdir): +def test_leptonica_compile(tmp_path): from ocrmypdf.lib.compile_leptonica import ffibuilder # Compile the library but build it somewhere that won't interfere with # existing compiled library. Also compile in API mode so that we test # the interfaces, even though we use it ABI mode. - ffibuilder.compile(tmpdir=fspath(tmpdir), target=fspath(tmpdir / 'lepttest.*')) + ffibuilder.compile(tmpdir=fspath(tmp_path), target=fspath(tmp_path / 'lepttest.*')) -def test_with_stderr(capsys): - # pytest redirects stderr too; we must disable this for the test to be valid - with capsys.disabled(): - with pytest.raises(FileNotFoundError): - lept.Pix.open("does_not_exist1") +def test_file_not_found(): + with pytest.raises(FileNotFoundError): + lp.Pix.open("does_not_exist1") -def test_without_stderr(capsys): - # pytest redirects stderr too; we must disable this for the test to be valid - with capsys.disabled(): - with patch('sys.stderr', new=None): - with pytest.raises(FileNotFoundError): - lept.Pix.open("does_not_exist2") +@pytest.mark.skipif( + lp.get_leptonica_version() < 'leptonica-1.79.0', + reason="test not reliable on all platforms for old leptonica", +) +def test_error_trap(): + with pytest.raises(lp.LeptonicaError, match=r"Error in pixReadMem"): + with lp._LeptonicaErrorTrap(): + lp.Pix(lp.lept.pixReadMem(lp.ffi.NULL, 0)) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..6bbd551f --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,21 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging + +import pytest + +from ocrmypdf._sync import configure_debug_logging + + +def test_debug_logging(tmp_path): + # Just exercise the debug logger but don't validate it + # See https://github.com/pytest-dev/pytest/issues/5502 for pytest logging quirks + prefix = 'test_debug_logging' + log = logging.getLogger(prefix) + handler = configure_debug_logging(tmp_path / 'test.log', prefix) + log.info("test message") + log.removeHandler(handler) diff --git a/tests/test_main.py b/tests/test_main.py index 248a9003..0d827fa3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,186 +1,53 @@ -# © 2015-17 James R. Barlow: github.com/jbarlow83 +# © 2015-19 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import logging import os import shutil -import sys from math import isclose from pathlib import Path -from subprocess import DEVNULL, PIPE, run, Popen +from subprocess import PIPE, run +from unittest.mock import patch +import pikepdf import PIL import pytest from PIL import Image +import ocrmypdf +from ocrmypdf._exec import ghostscript, tesseract from ocrmypdf.exceptions import ExitCode, MissingDependencyError -from ocrmypdf.exec import ghostscript, qpdf, tesseract, unpaper -from ocrmypdf.leptonica import Pix from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo +from ocrmypdf.subprocess import get_version -# pytest.helpers is dynamic -# pylint: disable=no-member,redefined-outer-name +from .conftest import ( + check_ocrmypdf, + first_page_dimensions, + have_unpaper, + is_macos, + run_ocrmypdf, + run_ocrmypdf_api, + running_in_docker, +) -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof +# pylint: disable=redefined-outer-name RENDERERS = ['hocr', 'sandwich'] -@pytest.fixture(scope='session') -def spoof_tesseract_crash(tmpdir_factory): - return spoof(tmpdir_factory, tesseract='tesseract_crash.py') - - -@pytest.fixture(scope='session') -def spoof_tesseract_big_image_error(tmpdir_factory): - return spoof(tmpdir_factory, tesseract='tesseract_big_image_error.py') - - -@pytest.fixture(scope='session') -def spoof_no_tess_no_pdfa(tmpdir_factory): - return spoof(tmpdir_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py') - - -@pytest.fixture(scope='session') -def spoof_no_tess_pdfa_warning(tmpdir_factory): - return spoof( - tmpdir_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py' - ) - - -@pytest.fixture(scope='session') -def spoof_no_tess_gs_render_fail(tmpdir_factory): - return spoof( - tmpdir_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py' - ) - - -@pytest.fixture(scope='session') -def spoof_no_tess_gs_raster_fail(tmpdir_factory): - return spoof( - tmpdir_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py' - ) - - -@pytest.fixture(scope='session') -def spoof_tess_bad_utf8(tmpdir_factory): - return spoof(tmpdir_factory, tesseract='tesseract_badutf8.py') - - -def test_quick(spoof_tesseract_cache, resources, outpdf): - check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_tesseract_cache) - - -def test_deskew(spoof_tesseract_noop, resources, outdir): - # Run with deskew - deskewed_pdf = check_ocrmypdf( - resources / 'skew.pdf', outdir / 'skew.pdf', '-d', env=spoof_tesseract_noop - ) - - # Now render as an image again and use Leptonica to find the skew angle - # to confirm that it was deskewed - log = logging.getLogger() - - deskewed_png = outdir / 'deskewed.png' - - ghostscript.rasterize_pdf( - deskewed_pdf, - deskewed_png, - xres=150, - yres=150, - raster_device='pngmono', - log=log, - pageno=1, - ) - - pix = Pix.open(deskewed_png) - skew_angle, skew_confidence = pix.find_skew() - - print(skew_angle) - assert -0.5 < skew_angle < 0.5, "Deskewing failed" - - -def test_remove_background(spoof_tesseract_noop, resources, outdir): - # Ensure the input image does not contain pure white/black - im = Image.open(resources / 'congress.jpg') - assert im.getextrema() != ((0, 255), (0, 255), (0, 255)) - - output_pdf = check_ocrmypdf( - resources / 'congress.jpg', - outdir / 'test_remove_bg.pdf', - '--remove-background', - '--image-dpi', - '150', - env=spoof_tesseract_noop, - ) - - log = logging.getLogger() - - output_png = outdir / 'remove_bg.png' - - ghostscript.rasterize_pdf( - output_pdf, - output_png, - xres=100, - yres=100, - raster_device='png16m', - log=log, - pageno=1, - ) - - # The output image should contain pure white and black - im = Image.open(output_png) - assert im.getextrema() == ((0, 255), (0, 255), (0, 255)) - - -# This will run 5 * 2 * 2 = 20 test cases -@pytest.mark.parametrize( - "pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'] -) -@pytest.mark.parametrize("renderer", ['sandwich', 'hocr']) -@pytest.mark.parametrize("output_type", ['pdf', 'pdfa']) -def test_exotic_image( - spoof_tesseract_cache, pdf, renderer, output_type, resources, outdir -): - outfile = outdir / f'test_{pdf}_{renderer}.pdf' +def test_quick(resources, outpdf): check_ocrmypdf( - resources / pdf, - outfile, - '-dc' if pytest.helpers.have_unpaper() else '-d', - '-v', - '1', - '--output-type', - output_type, - '--sidecar', - '--skip-text', - '--pdf-renderer', - renderer, - env=spoof_tesseract_cache, + resources / 'ccitt.pdf', outpdf, '--plugin', 'tests/plugins/tesseract_cache.py' ) - assert outfile.with_suffix('.pdf.txt').exists() - @pytest.mark.parametrize('renderer', RENDERERS) -def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf): +def test_oversample(renderer, resources, outpdf): oversampled_pdf = check_ocrmypdf( resources / 'skew.pdf', outpdf, @@ -189,49 +56,58 @@ def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf): '-f', '--pdf-renderer', renderer, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) pdfinfo = PdfInfo(oversampled_pdf) - print(pdfinfo[0].xres) - assert abs(pdfinfo[0].xres - 350) < 1 + print(pdfinfo[0].dpi.x) + assert abs(pdfinfo[0].dpi.x - 350) < 1 def test_repeat_ocr(resources, no_outpdf): - p, _, _ = run_ocrmypdf(resources / 'graph_ocred.pdf', no_outpdf) - assert p.returncode != 0 + result = run_ocrmypdf_api(resources / 'graph_ocred.pdf', no_outpdf) + assert result == ExitCode.already_done_ocr -def test_force_ocr(spoof_tesseract_cache, resources, outpdf): +def test_force_ocr(resources, outpdf): out = check_ocrmypdf( - resources / 'graph_ocred.pdf', outpdf, '-f', env=spoof_tesseract_cache + resources / 'graph_ocred.pdf', + outpdf, + '-f', + '--plugin', + 'tests/plugins/tesseract_cache.py', ) pdfinfo = PdfInfo(out) assert pdfinfo[0].has_text -def test_skip_ocr(spoof_tesseract_cache, resources, outpdf): +def test_skip_ocr(resources, outpdf): out = check_ocrmypdf( - resources / 'graph_ocred.pdf', outpdf, '-s', env=spoof_tesseract_cache + resources / 'graph_ocred.pdf', + outpdf, + '-s', + '--plugin', + 'tests/plugins/tesseract_cache.py', ) pdfinfo = PdfInfo(out) assert pdfinfo[0].has_text -@pytest.helpers.needs_pdfminer -def test_redo_ocr(spoof_tesseract_cache, resources, outpdf): +def test_redo_ocr(resources, outpdf): in_ = resources / 'graph_ocred.pdf' - before = PdfInfo(in_, detailed_page_analysis=True) - out = check_ocrmypdf(in_, outpdf, '--redo-ocr', env=spoof_tesseract_cache) - after = PdfInfo(out, detailed_page_analysis=True) + before = PdfInfo(in_, detailed_analysis=True) + out = outpdf + out = check_ocrmypdf(in_, out, '--redo-ocr') + after = PdfInfo(out, detailed_analysis=True) assert before[0].has_text and after[0].has_text assert ( before[0].get_textareas() != after[0].get_textareas() ), "Expected text to be different after re-OCR" -def test_argsfile(spoof_tesseract_noop, resources, outdir): +def test_argsfile(resources, outdir): path_argsfile = outdir / 'test_argsfile.txt' with open(str(path_argsfile), 'w') as argsfile: print( @@ -239,15 +115,14 @@ def test_argsfile(spoof_tesseract_noop, resources, outdir): 'ArgsFile Test', '--author', 'Test Cases', + '--plugin', + 'tests/plugins/tesseract_noop.py', sep='\n', end='\n', file=argsfile, ) check_ocrmypdf( - resources / 'graph.pdf', - path_argsfile, - '@' + str(outdir / 'test_argsfile.txt'), - env=spoof_tesseract_noop, + resources / 'graph.pdf', path_argsfile, '@' + str(outdir / 'test_argsfile.txt') ) @@ -265,9 +140,14 @@ def test_ocr_timeout(renderer, resources, outpdf): assert not pdfinfo[0].has_text -def test_skip_big(spoof_tesseract_cache, resources, outpdf): +def test_skip_big(resources, outpdf): out = check_ocrmypdf( - resources / 'jbig2.pdf', outpdf, '--skip-big', '1', env=spoof_tesseract_cache + resources / 'jbig2.pdf', + outpdf, + '--skip-big', + '1', + '--plugin', + 'tests/plugins/tesseract_cache.py', ) pdfinfo = PdfInfo(out) assert not pdfinfo[0].has_text @@ -275,14 +155,12 @@ def test_skip_big(spoof_tesseract_cache, resources, outpdf): @pytest.mark.parametrize('renderer', RENDERERS) @pytest.mark.parametrize('output_type', ['pdf', 'pdfa']) -def test_maximum_options( - spoof_tesseract_cache, renderer, output_type, resources, outpdf -): +def test_maximum_options(renderer, output_type, resources, outpdf): check_ocrmypdf( resources / 'multipage.pdf', outpdf, '-d', - '-ci' if pytest.helpers.have_unpaper() else None, + '-ci' if have_unpaper() else None, '-f', '-k', '--oversample', @@ -298,112 +176,130 @@ def test_maximum_options( renderer, '--output-type', output_type, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) -def test_tesseract_missing_tessdata(resources, no_outpdf): - env = os.environ.copy() - env['TESSDATA_PREFIX'] = '/tmp' - - p, _, err = run_ocrmypdf( - resources / 'graph_ocred.pdf', no_outpdf, '-v', '1', '--skip-text', env=env - ) - assert p.returncode == ExitCode.missing_dependency, err +def test_tesseract_missing_tessdata(monkeypatch, resources, no_outpdf, tmpdir): + monkeypatch.setenv("TESSDATA_PREFIX", os.fspath(tmpdir)) + with pytest.raises(MissingDependencyError): + run_ocrmypdf_api(resources / 'graph.pdf', no_outpdf, '-v', '1', '--skip-text') def test_invalid_input_pdf(resources, no_outpdf): - p, out, err = run_ocrmypdf(resources / 'invalid.pdf', no_outpdf) - assert p.returncode == ExitCode.input_file, err + result = run_ocrmypdf_api(resources / 'invalid.pdf', no_outpdf) + assert result == ExitCode.input_file def test_blank_input_pdf(resources, outpdf): - p, out, err = run_ocrmypdf(resources / 'blank.pdf', outpdf) - assert p.returncode == ExitCode.ok + result = run_ocrmypdf_api(resources / 'blank.pdf', outpdf) + assert result == ExitCode.ok -def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources, no_outpdf): +def test_force_ocr_on_pdf_with_no_images(resources, no_outpdf): # As a correctness test, make sure that --force-ocr on a PDF with no # content still triggers tesseract. If tesseract crashes, then it was # called. - p, _, err = run_ocrmypdf( - resources / 'blank.pdf', no_outpdf, '--force-ocr', env=spoof_tesseract_crash + p, _, _ = run_ocrmypdf( + resources / 'blank.pdf', + no_outpdf, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_crash.py', ) - assert p.returncode == ExitCode.child_process_error, err - assert not os.path.exists(no_outpdf) + assert p.returncode == ExitCode.child_process_error + assert not no_outpdf.exists() @pytest.mark.skipif( - pytest.helpers.is_macos() and pytest.helpers.running_in_travis(), - reason="takes too long to install language packs in Travis macOS homebrew", + is_macos(), + reason="takes too long to install language packs in macOS homebrew", ) -def test_german(spoof_tesseract_cache, resources, outdir): +def test_german(resources, outdir): # Produce a sidecar too - implicit test that system locale is set up # properly. It is fine that we are testing -l deu on a French file because # we are exercising the functionality not going for accuracy. sidecar = outdir / 'francais.txt' - p, out, err = run_ocrmypdf( - resources / 'francais.pdf', - outdir / 'francais.pdf', - '-l', - 'deu', # more commonly installed - '--sidecar', - sidecar, - env=spoof_tesseract_cache, - ) - print(os.environ) - assert ( - p.returncode == ExitCode.ok - ), "This test may fail if Tesseract language packs are missing" + try: + check_ocrmypdf( + resources / 'francais.pdf', + outdir / 'francais.pdf', + '-l', + 'deu', # more commonly installed + '--sidecar', + sidecar, + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) + except MissingDependencyError: + if 'deu' not in tesseract.get_languages(): + pytest.xfail(reason="tesseract-deu language pack not installed") + raise def test_klingon(resources, outpdf): - p, out, err = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz') + p, _, _ = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz') assert p.returncode == ExitCode.missing_dependency -def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf): - p, out, err = run_ocrmypdf( +def test_missing_docinfo(resources, outpdf): + result = run_ocrmypdf_api( resources / 'missing_docinfo.pdf', outpdf, '-l', 'eng', '--skip-text', - env=spoof_tesseract_noop, + '--plugin', + Path('tests/plugins/tesseract_noop.py'), ) - assert p.returncode == ExitCode.ok, err + assert result == ExitCode.ok -def test_uppercase_extension(spoof_tesseract_noop, resources, outdir): +def test_uppercase_extension(resources, outdir): shutil.copy(str(resources / "skew.pdf"), str(outdir / "UPPERCASE.PDF")) check_ocrmypdf( - outdir / "UPPERCASE.PDF", outdir / "UPPERCASE_OUT.PDF", env=spoof_tesseract_noop + outdir / "UPPERCASE.PDF", + outdir / "UPPERCASE_OUT.PDF", + '--plugin', + 'tests/plugins/tesseract_noop.py', ) -def test_input_file_not_found(no_outpdf): +def test_input_file_not_found(caplog, no_outpdf): input_file = "does not exist.pdf" - p, out, err = run_ocrmypdf(input_file, no_outpdf) - assert p.returncode == ExitCode.input_file - assert input_file in out or input_file in err + result = run_ocrmypdf_api(input_file, no_outpdf) + assert result == ExitCode.input_file + assert input_file in caplog.text -def test_input_file_not_a_pdf(no_outpdf): +@pytest.mark.skipif(os.name == 'nt' or running_in_docker(), reason="chmod") +def test_input_file_not_readable(caplog, resources, outdir, no_outpdf): + input_file = outdir / 'trivial.pdf' + shutil.copy(resources / 'trivial.pdf', input_file) + input_file.chmod(0o000) + result = run_ocrmypdf_api(input_file, no_outpdf) + assert result == ExitCode.input_file + assert str(input_file) in caplog.text + + +def test_input_file_not_a_pdf(caplog, no_outpdf): input_file = __file__ # Try to OCR this file - p, out, err = run_ocrmypdf(input_file, no_outpdf) - assert p.returncode == ExitCode.input_file - assert input_file in out or input_file in err + result = run_ocrmypdf_api(input_file, no_outpdf) + assert result == ExitCode.input_file + if os.name != 'nt': # name will be mangled with \\'s on nt + assert input_file in caplog.text -def test_encrypted(resources, no_outpdf): - p, out, err = run_ocrmypdf(resources / 'skew-encrypted.pdf', no_outpdf) - assert p.returncode == ExitCode.encrypted_pdf - assert out.find('encrypted') +def test_encrypted(resources, caplog, no_outpdf): + result = run_ocrmypdf_api(resources / 'skew-encrypted.pdf', no_outpdf) + assert result == ExitCode.encrypted_pdf + assert 'encryption must be removed' in caplog.text @pytest.mark.parametrize('renderer', RENDERERS) -def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf): +def test_pagesegmode(renderer, resources, outpdf): check_ocrmypdf( resources / 'skew.pdf', outpdf, @@ -413,41 +309,46 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf): '1', '--pdf-renderer', renderer, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) @pytest.mark.parametrize('renderer', RENDERERS) -def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf): - p, out, err = run_ocrmypdf( +def test_tesseract_crash(renderer, resources, no_outpdf): + p, _, err = run_ocrmypdf( resources / 'ccitt.pdf', no_outpdf, '-v', '1', '--pdf-renderer', renderer, - env=spoof_tesseract_crash, + '--plugin', + 'tests/plugins/tesseract_crash.py', ) assert p.returncode == ExitCode.child_process_error - assert not os.path.exists(no_outpdf) - assert "ERROR" in err + assert not no_outpdf.exists() + assert "SubprocessOutputError" in err -def test_tesseract_crash_autorotate(spoof_tesseract_crash, resources, no_outpdf): +def test_tesseract_crash_autorotate(resources, no_outpdf): p, out, err = run_ocrmypdf( - resources / 'ccitt.pdf', no_outpdf, '-r', env=spoof_tesseract_crash + resources / 'ccitt.pdf', + no_outpdf, + '-r', + '--plugin', + 'tests/plugins/tesseract_crash.py', ) assert p.returncode == ExitCode.child_process_error - assert not os.path.exists(no_outpdf) - assert "ERROR" in err + assert not no_outpdf.exists() + assert "uncaught exception" in err print(out) print(err) @pytest.mark.parametrize('renderer', RENDERERS) -def test_tesseract_image_too_big( - renderer, spoof_tesseract_big_image_error, resources, outpdf -): +@pytest.mark.slow +def test_tesseract_image_too_big(renderer, resources, outpdf): check_ocrmypdf( resources / 'hugemono.pdf', outpdf, @@ -456,82 +357,22 @@ def test_tesseract_image_too_big( renderer, '--max-image-mpixels', '0', - env=spoof_tesseract_big_image_error, + '--plugin', + 'tests/plugins/tesseract_big_image_error.py', ) -def test_algo4(resources, spoof_tesseract_noop, outpdf): +def test_algo4(resources, outpdf): p, _, _ = run_ocrmypdf( - resources / 'encrypted_algo4.pdf', outpdf, env=spoof_tesseract_noop + resources / 'encrypted_algo4.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.encrypted_pdf -@pytest.mark.parametrize('renderer', RENDERERS) -def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf): - # Confirm input image is non-square resolution - in_pageinfo = PdfInfo(resources / 'aspect.pdf') - assert in_pageinfo[0].xres != in_pageinfo[0].yres - - check_ocrmypdf( - resources / 'aspect.pdf', - outpdf, - '--pdf-renderer', - renderer, - env=spoof_tesseract_cache, - ) - - out_pageinfo = PdfInfo(outpdf) - - # Confirm resolution was kept the same - assert in_pageinfo[0].xres == out_pageinfo[0].xres - assert in_pageinfo[0].yres == out_pageinfo[0].yres - - -@pytest.mark.parametrize('renderer', RENDERERS) -def test_convert_to_square_resolution( - renderer, spoof_tesseract_cache, resources, outpdf -): - # Confirm input image is non-square resolution - in_pageinfo = PdfInfo(resources / 'aspect.pdf') - assert in_pageinfo[0].xres != in_pageinfo[0].yres - - # --force-ocr requires means forced conversion to square resolution - check_ocrmypdf( - resources / 'aspect.pdf', - outpdf, - '--force-ocr', - '--pdf-renderer', - renderer, - env=spoof_tesseract_cache, - ) - - out_pageinfo = PdfInfo(outpdf) - - in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0] - - # Resolution show now be equal - assert out_p0.xres == out_p0.yres - - # Page size should match input page size - assert isclose(in_p0.width_inches, out_p0.width_inches) - assert isclose(in_p0.height_inches, out_p0.height_inches) - - # Because we rasterized the page to produce a new image, it should occupy - # the entire page - out_im_w = out_p0.images[0].width / out_p0.images[0].xres - out_im_h = out_p0.images[0].height / out_p0.images[0].yres - assert isclose(out_p0.width_inches, out_im_w) - assert isclose(out_p0.height_inches, out_im_h) - - -def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf): - check_ocrmypdf( - resources / 'crom.png', outpdf, '--image-dpi', '200', env=spoof_tesseract_noop - ) - - -def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf): +def test_jbig2_passthrough(resources, outpdf): out = check_ocrmypdf( resources / 'jbig2.pdf', outpdf, @@ -539,123 +380,64 @@ def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf): 'pdf', '--pdf-renderer', 'hocr', - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) out_pageinfo = PdfInfo(out) assert out_pageinfo[0].images[0].enc == Encoding.jbig2 -def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf): - input_file = str(resources / 'francais.pdf') - output_file = str(outpdf) - - # Runs: ocrmypdf - output.pdf < testfile.pdf - with open(input_file, 'rb') as input_stream: - p_args = ocrmypdf_exec + ['-', output_file] - p = run( - p_args, - stdout=PIPE, - stderr=PIPE, - stdin=input_stream, - env=spoof_tesseract_noop, - ) - assert p.returncode == ExitCode.ok - - -def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf): - input_file = str(resources / 'francais.pdf') - output_file = str(outpdf) - - # Runs: ocrmypdf francais.pdf - > test_stdout.pdf - with open(output_file, 'wb') as output_stream: - p_args = ocrmypdf_exec + [input_file, '-'] - p = run( - p_args, - stdout=output_stream, - stderr=PIPE, - stdin=DEVNULL, - env=spoof_tesseract_noop, - ) - assert p.returncode == ExitCode.ok - - assert qpdf.check(output_file, log=None) - - -@pytest.mark.skipif( - sys.version_info[0:3] >= (3, 6, 4), reason="issue fixed in Python 3.6.4" -) -def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf): - input_file = str(resources / 'francais.pdf') - output_file = str(outpdf) - - def evil_closer(): - os.close(0) - os.close(1) - - p_args = ocrmypdf_exec + [input_file, output_file] - p = Popen( # pylint: disable=subprocess-popen-preexec-fn - p_args, - close_fds=True, - stdout=None, - stderr=PIPE, - stdin=None, - env=spoof_tesseract_noop, - preexec_fn=evil_closer, - ) - out, err = p.communicate() - print(err.decode()) - assert p.returncode == ExitCode.ok - - -def test_masks(spoof_tesseract_noop, resources, outpdf): - p, out, err = run_ocrmypdf( - resources / 'masks.pdf', outpdf, env=spoof_tesseract_noop - ) - - assert p.returncode == ExitCode.ok - - -def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop, resources, outpdf): - check_ocrmypdf(resources / 'epson.pdf', outpdf, env=spoof_tesseract_noop) - - -def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf): - p, out, err = run_ocrmypdf( - resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa - ) +def test_masks(resources, outpdf): assert ( - p.returncode == ExitCode.pdfa_conversion_failed - ), "Unexpected return when PDF/A fails" + ocrmypdf.ocr( + resources / 'masks.pdf', outpdf, plugins=['tests/plugins/tesseract_noop.py'] + ) + == ExitCode.ok + ) -def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf): - check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning) +def test_linearized_pdf_and_indirect_object(resources, outpdf): + check_ocrmypdf( + resources / 'epson.pdf', outpdf, '--plugin', 'tests/plugins/tesseract_noop.py' + ) -def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf): +def test_very_high_dpi(resources, outpdf): "Checks for a Decimal quantize error with high DPI, etc" - check_ocrmypdf(resources / '2400dpi.pdf', outpdf, env=spoof_tesseract_cache) + check_ocrmypdf( + resources / '2400dpi.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) pdfinfo = PdfInfo(outpdf) image = pdfinfo[0].images[0] - assert isclose(image.xres, image.yres) - assert isclose(image.xres, 2400) + assert isclose(image.dpi.x, image.dpi.y) + assert isclose(image.dpi.x, 2400) -def test_overlay(spoof_tesseract_noop, resources, outpdf): +def test_overlay(resources, outpdf): check_ocrmypdf( - resources / 'overlay.pdf', outpdf, '--skip-text', env=spoof_tesseract_noop + resources / 'overlay.pdf', + outpdf, + '--skip-text', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) -def test_destination_not_writable(spoof_tesseract_noop, resources, outdir): - if os.getuid() == 0 or os.geteuid() == 0: +def test_destination_not_writable(resources, outdir): + if os.name != 'nt' and (os.getuid() == 0 or os.geteuid() == 0): pytest.xfail(reason="root can write to anything") protected_file = outdir / 'protected.pdf' protected_file.touch() protected_file.chmod(0o400) # Read-only - p, out, err = run_ocrmypdf( - resources / 'jbig2.pdf', protected_file, env=spoof_tesseract_noop + p, _out, _err = run_ocrmypdf( + resources / 'jbig2.pdf', + protected_file, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.file_access_error, "Expected error" @@ -672,26 +454,16 @@ language_model_penalty_non_freq_dict_word 0 ) check_ocrmypdf( - resources / 'ccitt.pdf', outdir / 'out.pdf', '--tesseract-config', cfg_file - ) - - -@pytest.mark.parametrize('renderer', RENDERERS) -def test_tesseract_config_notfound(renderer, resources, outdir): - cfg_file = outdir / 'nofile.cfg' - - p, out, err = run_ocrmypdf( - resources / 'ccitt.pdf', + resources / '3small.pdf', outdir / 'out.pdf', - '--pdf-renderer', - renderer, '--tesseract-config', cfg_file, + '--pages', + '1', ) - assert "Can't open" in err, "No error message about missing config file" - assert p.returncode == ExitCode.ok, err +@pytest.mark.slow # This test sometimes times out in CI @pytest.mark.parametrize('renderer', RENDERERS) def test_tesseract_config_invalid(renderer, resources, outdir): cfg_file = outdir / 'test.cfg' @@ -702,7 +474,7 @@ THIS FILE IS INVALID ''' ) - p, out, err = run_ocrmypdf( + p, _out, err = run_ocrmypdf( resources / 'ccitt.pdf', outdir / 'out.pdf', '--pdf-renderer', @@ -710,30 +482,18 @@ THIS FILE IS INVALID '--tesseract-config', cfg_file, ) - assert "parameter not found" in err.lower(), "No error message" + assert ( + "parameter not found" in err.lower() + or "error occurred while parsing" in err.lower() + ), "No error message" assert p.returncode == ExitCode.invalid_config -@pytest.mark.skipif(tesseract.v4(), reason='arg has no effect in 4.0-beta1') -def test_user_words(resources, outdir): +@pytest.mark.skipif(not tesseract.has_user_words(), reason='not functional until 4.1.0') +def test_user_words_ocr(resources, outdir): + # Does not actually test if --user-words causes output to differ word_list = outdir / 'wordlist.txt' - sidecar_before = outdir / 'sidecar_before.txt' - sidecar_after = outdir / 'sidecar_after.txt' - - # Don't know how to make this test pass on various versions and platforms - # so weaken to merely testing that the argument is accepted - consistent = False - - if consistent: - check_ocrmypdf( - resources / 'crom.png', - outdir / 'out.pdf', - '--image-dpi', - 150, - '--sidecar', - sidecar_before, - ) - assert 'cromulent' not in sidecar_before.open().read() + sidecar_after = outdir / 'sidecar.txt' with word_list.open('w') as f: f.write('cromulent\n') # a perfectly cromulent word @@ -749,22 +509,20 @@ def test_user_words(resources, outdir): word_list, ) - if consistent: - assert 'cromulent' in sidecar_after.open().read() - -def test_form_xobject(spoof_tesseract_noop, resources, outpdf): +def test_form_xobject(resources, outpdf): check_ocrmypdf( - resources / 'formxobject.pdf', outpdf, '--force-ocr', env=spoof_tesseract_noop + resources / 'formxobject.pdf', + outpdf, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) @pytest.mark.parametrize('renderer', RENDERERS) def test_pagesize_consistency(renderer, resources, outpdf): - - first_page_dimensions = pytest.helpers.first_page_dimensions - - infile = resources / 'linn.pdf' + infile = resources / '3small.pdf' before_dims = first_page_dimensions(infile) @@ -773,61 +531,50 @@ def test_pagesize_consistency(renderer, resources, outpdf): outpdf, '--pdf-renderer', renderer, - '--clean' if pytest.helpers.have_unpaper() else None, + '--clean' if have_unpaper() else None, '--deskew', '--remove-background', - '--clean-final' if pytest.helpers.have_unpaper() else None, + '--clean-final' if have_unpaper() else None, + '--pages', + '1', ) after_dims = first_page_dimensions(outpdf) - assert isclose(before_dims[0], after_dims[0]) - assert isclose(before_dims[1], after_dims[1]) + assert isclose(before_dims[0], after_dims[0], rel_tol=1e-4) + assert isclose(before_dims[1], after_dims[1], rel_tol=1e-4) -def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf): +def test_skip_big_with_no_images(resources, outpdf): check_ocrmypdf( resources / 'blank.pdf', outpdf, '--skip-big', '5', '--force-ocr', - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) -def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf): - p, out, err = run_ocrmypdf( - resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail - ) - print(err) - assert p.returncode == ExitCode.child_process_error - - -def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf): - p, out, err = run_ocrmypdf( - resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_gs_raster_fail - ) - print(err) - assert p.returncode == ExitCode.child_process_error - - @pytest.mark.skipif( - '8.0.0' <= qpdf.version() <= '8.0.1', - reason="qpdf regression on pages with no contents", + '8.0.0' <= pikepdf.__libqpdf_version__ <= '8.0.1', + reason="libqpdf regression on pages with no contents", ) -def test_no_contents(spoof_tesseract_noop, resources, outpdf): +def test_no_contents(resources, outpdf): check_ocrmypdf( - resources / 'no_contents.pdf', outpdf, '--force-ocr', env=spoof_tesseract_noop + resources / 'no_contents.pdf', + outpdf, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) @pytest.mark.parametrize( 'image', ['baiona.png', 'baiona_gray.png', 'baiona_alpha.png', 'congress.jpg'] ) -def test_compression_preserved( - spoof_tesseract_noop, ocrmypdf_exec, resources, image, outpdf -): +def test_compression_preserved(ocrmypdf_exec, resources, image, outpdf): input_file = str(resources / image) output_file = str(outpdf) @@ -841,6 +588,8 @@ def test_compression_preserved( '150', '--output-type', 'pdf', + '--plugin', + 'tests/plugins/tesseract_noop.py', '-', output_file, ] @@ -849,8 +598,8 @@ def test_compression_preserved( stdout=PIPE, stderr=PIPE, stdin=input_stream, - universal_newlines=True, - env=spoof_tesseract_noop, + universal_newlines=True, # When dropping support for Python 3.6 change to text= + check=False, ) if im.mode in ('RGBA', 'LA'): @@ -872,6 +621,7 @@ def test_compression_preserved( assert pdfimage.color == Colorspace.rgb, "Colorspace changed" elif im.mode.startswith('L'): assert pdfimage.color == Colorspace.gray, "Colorspace changed" + im.close() @pytest.mark.parametrize( @@ -882,9 +632,7 @@ def test_compression_preserved( ('congress.jpg', 'lossless'), ], ) -def test_compression_changed( - spoof_tesseract_noop, ocrmypdf_exec, resources, image, compression, outpdf -): +def test_compression_changed(ocrmypdf_exec, resources, image, compression, outpdf): input_file = str(resources / image) output_file = str(outpdf) @@ -901,6 +649,8 @@ def test_compression_changed( '0', '--pdfa-image-compression', compression, + '--plugin', + 'tests/plugins/tesseract_noop.py', '-', output_file, ] @@ -909,8 +659,8 @@ def test_compression_changed( stdout=PIPE, stderr=PIPE, stdin=input_stream, - universal_newlines=True, - env=spoof_tesseract_noop, + universal_newlines=True, # When dropping support for Python 3.6 change to text= + check=False, ) assert p.returncode == ExitCode.ok, p.stderr @@ -933,23 +683,25 @@ def test_compression_changed( assert pdfimage.color == Colorspace.rgb, "Colorspace changed" elif im.mode.startswith('L'): assert pdfimage.color == Colorspace.gray, "Colorspace changed" + im.close() -def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf): - sidecar = outpdf + '.txt' +def test_sidecar_pagecount(resources, outpdf): + sidecar = outpdf.with_suffix('.txt') check_ocrmypdf( - resources / 'multipage.pdf', + resources / '3small.pdf', outpdf, '--skip-text', '--sidecar', sidecar, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) - pdfinfo = PdfInfo(resources / 'multipage.pdf') + pdfinfo = PdfInfo(resources / '3small.pdf') num_pages = len(pdfinfo) - with open(sidecar, 'r') as f: + with open(sidecar, 'r', encoding='utf-8') as f: ocr_text = f.read() # There should a formfeed between each pair of pages, so the count of @@ -959,19 +711,24 @@ def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf): ), "Sidecar page count does not match PDF page count" -def test_sidecar_nonempty(spoof_tesseract_cache, resources, outpdf): - sidecar = outpdf + '.txt' +def test_sidecar_nonempty(resources, outpdf): + sidecar = outpdf.with_suffix('.txt') check_ocrmypdf( - resources / 'ccitt.pdf', outpdf, '--sidecar', sidecar, env=spoof_tesseract_cache + resources / 'ccitt.pdf', + outpdf, + '--sidecar', + sidecar, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) - with open(sidecar, 'r') as f: + with open(sidecar, 'r', encoding='utf-8') as f: ocr_text = f.read() assert 'the' in ocr_text @pytest.mark.parametrize('pdfa_level', ['1', '2', '3']) -def test_pdfa_n(spoof_tesseract_cache, pdfa_level, resources, outpdf): +def test_pdfa_n(pdfa_level, resources, outpdf): if pdfa_level == '3' and ghostscript.version() < '9.19': pytest.xfail(reason='Ghostscript >= 9.19 required') @@ -980,91 +737,74 @@ def test_pdfa_n(spoof_tesseract_cache, pdfa_level, resources, outpdf): outpdf, '--output-type', 'pdfa-' + pdfa_level, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) pdfa_info = file_claims_pdfa(outpdf) assert pdfa_info['conformance'] == f'PDF/A-{pdfa_level}B' -@pytest.mark.skipif(sys.version_info >= (3, 7, 0), reason='better utf-8') -@pytest.mark.skipif( - Path('/etc/alpine-release').exists(), reason="invalid test on alpine" -) -def test_bad_locale(): - env = os.environ.copy() - env['LC_ALL'] = 'C' - - p, out, err = run_ocrmypdf('a', 'b', env=env) - assert out == '', "stdout not clean" - assert p.returncode != 0 - assert 'configured to use ASCII as encoding' in err, "should whine" - - -@pytest.mark.parametrize('renderer', RENDERERS) -def test_bad_utf8(spoof_tess_bad_utf8, renderer, resources, no_outpdf): - p, out, err = run_ocrmypdf( - resources / 'ccitt.pdf', - no_outpdf, - '--pdf-renderer', - renderer, - env=spoof_tess_bad_utf8, - ) - - assert out == '', "stdout not clean" - assert p.returncode != 0 - assert 'not utf-8' in err, "should whine about utf-8" - assert '\\x96' in err, 'should repeat backslash encoded output' - - @pytest.mark.skipif( PIL.__version__ < '5.0.0', reason="Pillow < 5.0.0 doesn't raise the exception" ) +@pytest.mark.slow def test_decompression_bomb(resources, outpdf): - p, out, err = run_ocrmypdf(resources / 'hugemono.pdf', outpdf) + p, _out, err = run_ocrmypdf(resources / 'hugemono.pdf', outpdf) assert 'decompression bomb' in err - p, out, err = run_ocrmypdf( + p, _out, err = run_ocrmypdf( resources / 'hugemono.pdf', outpdf, '--max-image-mpixels', '2000' ) assert p.returncode == 0 -def test_text_curves(spoof_tesseract_noop, resources, outpdf): - check_ocrmypdf(resources / 'vector.pdf', outpdf, env=spoof_tesseract_noop) +def test_text_curves(resources, outpdf): + with patch('ocrmypdf._pipeline.VECTOR_PAGE_DPI', 100): + check_ocrmypdf( + resources / 'vector.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) - info = PdfInfo(outpdf) - assert len(info.pages[0].images) == 0, "added images to the vector PDF" + info = PdfInfo(outpdf) + assert len(info.pages[0].images) == 0, "added images to the vector PDF" - check_ocrmypdf( - resources / 'vector.pdf', outpdf, '--force-ocr', env=spoof_tesseract_noop - ) + check_ocrmypdf( + resources / 'vector.pdf', + outpdf, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) - info = PdfInfo(outpdf) - assert len(info.pages[0].images) != 0, "force did not rasterize" + info = PdfInfo(outpdf) + assert len(info.pages[0].images) != 0, "force did not rasterize" -def test_dev_null(spoof_tesseract_noop, resources): - p, out, err = run_ocrmypdf( - resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop - ) - assert p.returncode == 0, "could not send output to /dev/null" - assert len(out) == 0, "wrote to stdout" - - -def test_output_is_dir(spoof_tesseract_noop, resources, outdir): - p, out, err = run_ocrmypdf( - resources / 'trivial.pdf', outdir, '--force-ocr', env=spoof_tesseract_noop +def test_output_is_dir(resources, outdir): + p, _out, err = run_ocrmypdf( + resources / 'trivial.pdf', + outdir, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.file_access_error assert 'is not a writable file' in err -def test_output_is_symlink(spoof_tesseract_noop, resources, outdir): +@pytest.mark.skipif(os.name == 'nt', reason="symlink needs admin permissions") +def test_output_is_symlink(resources, outdir): sym = Path(outdir / 'this_is_a_symlink') sym.symlink_to(outdir / 'out.pdf') - p, out, err = run_ocrmypdf( - resources / 'trivial.pdf', sym, '--force-ocr', env=spoof_tesseract_noop + p, _out, err = run_ocrmypdf( + resources / 'trivial.pdf', + sym, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.ok, err assert (outdir / 'out.pdf').stat().st_size > 0, 'target file not created' @@ -1077,8 +817,6 @@ def test_livecycle(resources, no_outpdf): def test_version_check(): - from ocrmypdf.exec import get_version - with pytest.raises(MissingDependencyError): get_version('NOT_FOUND_UNLIKELY_ON_PATH') @@ -1087,3 +825,59 @@ def test_version_check(): with pytest.raises(MissingDependencyError): get_version('echo') + + +@pytest.mark.parametrize( + 'threshold, optimize, output_type, expected', + [ + [1.0, 0, 'pdfa', False], + [1.0, 0, 'pdf', False], + [0.0, 0, 'pdfa', True], + [0.0, 0, 'pdf', True], + [1.0, 1, 'pdfa', False], + [1.0, 1, 'pdf', False], + [0.0, 1, 'pdfa', True], + [0.0, 1, 'pdf', True], + ], +) +def test_fast_web_view(resources, outpdf, threshold, optimize, output_type, expected): + check_ocrmypdf( + resources / 'trivial.pdf', + outpdf, + '--fast-web-view', + threshold, + '--optimize', + optimize, + '--output-type', + output_type, + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + with pikepdf.open(outpdf) as pdf: + assert pdf.is_linearized == expected + + +def test_image_dpi_not_image(caplog, resources, outpdf): + check_ocrmypdf( + resources / 'trivial.pdf', + outpdf, + '--image-dpi', + '100', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + assert '--image-dpi is being ignored' in caplog.text + + +def test_image_dpi_threshold(resources, outpdf): + check_ocrmypdf( + resources / 'typewriter.png', + outpdf, + '--threshold', + '--image-dpi=170', + '--output-type=pdf', + '--optimize=0', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + assert outpdf.exists() diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 442c71a1..ebcbf031 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,56 +1,41 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. import datetime from datetime import timezone -import logging -import mmap from os import fspath -from pathlib import Path from shutil import copyfile -from unittest.mock import MagicMock, patch - -import pytest +from unittest.mock import patch import pikepdf -from ocrmypdf._jobcontext import JobContext +import pytest +from pikepdf.models.metadata import decode_pdf_date + +from ocrmypdf._jobcontext import PdfContext +from ocrmypdf._pipeline import convert_to_pdfa, metadata_fixup +from ocrmypdf._plugin_manager import get_plugin_manager +from ocrmypdf.cli import get_parser from ocrmypdf.exceptions import ExitCode from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps -from pikepdf.models.metadata import decode_pdf_date +from ocrmypdf.pdfinfo import PdfInfo + +from .conftest import check_ocrmypdf, run_ocrmypdf try: import fitz except ImportError: fitz = None -# pytest.helpers is dynamic -# pylint: disable=no-member -# pylint: disable=w0612 pytestmark = pytest.mark.filterwarnings('ignore:.*XMLParser.*:DeprecationWarning') -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof - @pytest.mark.parametrize("output_type", ['pdfa', 'pdf']) -def test_preserve_metadata(spoof_tesseract_noop, output_type, resources, outpdf): +def test_preserve_docinfo(output_type, resources, outpdf): pdf_before = pikepdf.open(resources / 'graph.pdf') output = check_ocrmypdf( @@ -58,7 +43,8 @@ def test_preserve_metadata(spoof_tesseract_noop, output_type, resources, outpdf) outpdf, '--output-type', output_type, - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) pdf_after = pikepdf.open(output) @@ -71,12 +57,12 @@ def test_preserve_metadata(spoof_tesseract_noop, output_type, resources, outpdf) @pytest.mark.parametrize("output_type", ['pdfa', 'pdf']) -def test_override_metadata(spoof_tesseract_noop, output_type, resources, outpdf): +def test_override_metadata(output_type, resources, outpdf): input_file = resources / 'c02-22.pdf' german = 'Du siehst den Wald vor lauter Bäumen nicht.' chinese = '孔子' - p, out, err = run_ocrmypdf( + p, _out, err = run_ocrmypdf( input_file, outpdf, '--title', @@ -85,7 +71,8 @@ def test_override_metadata(spoof_tesseract_noop, output_type, resources, outpdf) chinese, '--output-type', output_type, - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.ok, err @@ -105,21 +92,22 @@ def test_override_metadata(spoof_tesseract_noop, output_type, resources, outpdf) assert pdfa_info['output'] == output_type -def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf): +def test_high_unicode(resources, no_outpdf): # Ghostscript doesn't support high Unicode, so neither do we, to be # safe input_file = resources / 'c02-22.pdf' high_unicode = 'U+1030C is: 𐌌' - p, out, err = run_ocrmypdf( + p, _out, err = run_ocrmypdf( input_file, no_outpdf, '--subject', high_unicode, '--output-type', 'pdfa', - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert p.returncode == ExitCode.bad_args, err @@ -128,9 +116,7 @@ def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf): @pytest.mark.skipif(not fitz, reason="test uses fitz") @pytest.mark.parametrize('ocr_option', ['--skip-text', '--force-ocr']) @pytest.mark.parametrize('output_type', ['pdf', 'pdfa']) -def test_bookmarks_preserved( - spoof_tesseract_noop, output_type, ocr_option, resources, outpdf -): +def test_bookmarks_preserved(output_type, ocr_option, resources, outpdf): input_file = resources / 'toc.pdf' before_toc = fitz.Document(str(input_file)).getToC() @@ -140,7 +126,8 @@ def test_bookmarks_preserved( ocr_option, '--output-type', output_type, - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) after_toc = fitz.Document(str(outpdf)).getToC() @@ -155,13 +142,16 @@ def seconds_between_dates(date1, date2): @pytest.mark.parametrize('infile', ['trivial.pdf', 'jbig2.pdf']) @pytest.mark.parametrize('output_type', ['pdf', 'pdfa']) -def test_creation_date_preserved( - spoof_tesseract_noop, output_type, resources, infile, outpdf -): +def test_creation_date_preserved(output_type, resources, infile, outpdf): input_file = resources / infile check_ocrmypdf( - input_file, outpdf, '--output-type', output_type, env=spoof_tesseract_noop + input_file, + outpdf, + '--output-type', + output_type, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) pdf_before = pikepdf.open(input_file) @@ -183,20 +173,33 @@ def test_creation_date_preserved( assert seconds_between_dates(date_after, datetime.datetime.now(timezone.utc)) < 1000 -@pytest.mark.parametrize('output_type', ['pdf', 'pdfa']) -def test_xml_metadata_preserved(spoof_tesseract_noop, output_type, resources, outpdf): - input_file = resources / 'graph.pdf' +@pytest.mark.parametrize( + 'test_file,output_type', + [ + ('graph.pdf', 'pdf'), # PDF with full metadata + ('graph.pdf', 'pdfa'), # PDF/A with full metadata + ('overlay.pdf', 'pdfa'), # /Title() + ('3small.pdf', 'pdfa'), + ], +) +def test_xml_metadata_preserved(test_file, output_type, resources, outpdf): + input_file = resources / test_file try: - from libxmp import consts - from libxmp.utils import file_to_dict - except Exception: + from libxmp.utils import file_to_dict # pylint: disable=import-outside-toplevel + except Exception: # pylint: disable=broad-except pytest.skip("libxmp not available or libexempi3 not installed") before = file_to_dict(str(input_file)) check_ocrmypdf( - input_file, outpdf, '--output-type', output_type, env=spoof_tesseract_noop + input_file, + outpdf, + '--output-type', + output_type, + '--skip-text', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) after = file_to_dict(str(outpdf)) @@ -218,6 +221,7 @@ def test_xml_metadata_preserved(spoof_tesseract_noop, output_type, resources, ou 'dc:type', 'pdf:keywords', ] + acquired_properties = ['dc:format'] might_change_properties = [ 'dc:date', 'pdf:pdfversion', @@ -251,8 +255,10 @@ def test_xml_metadata_preserved(spoof_tesseract_noop, output_type, resources, ou assert prop in after, f'{prop} dropped from xmp' assert before[prop] == after[prop] - # Certain entries like title appear as dc:title[1], with the possibility - # of several + # libxmp presents multivalued entries (e.g. dc:title) as: + # 'dc:title': '' <- there's a title + # 'dc:title[1]: 'The Title' <- the actual title + # 'dc:title[1]/?xml:lang': 'x-default' <- language info propidx = f'{prop}[1]' if propidx in before: assert ( @@ -260,11 +266,17 @@ def test_xml_metadata_preserved(spoof_tesseract_noop, output_type, resources, ou or after.get(prop) == before[propidx] ) + if prop in after and prop not in before: + assert prop in acquired_properties, ( + f"acquired unexpected property {prop} with value " + f"{after.get(propidx) or after.get(prop)}" + ) -def test_srgb_in_unicode_path(tmpdir): + +def test_srgb_in_unicode_path(tmp_path): """Test that we can produce pdfmark when install path is not ASCII""" - dstdir = Path(fspath(tmpdir)) / b'\xe4\x80\x80'.decode('utf-8') + dstdir = tmp_path / b'\xe4\x80\x80'.decode('utf-8') dstdir.mkdir() dst = dstdir / 'sRGB.icc' @@ -274,90 +286,108 @@ def test_srgb_in_unicode_path(tmpdir): generate_pdfa_ps(dstdir / 'out.ps') -def test_kodak_toc(resources, outpdf, spoof_tesseract_noop): - output = check_ocrmypdf( - resources / 'kcs.pdf', outpdf, '--output-type', 'pdf', env=spoof_tesseract_noop +def test_kodak_toc(resources, outpdf): + _output = check_ocrmypdf( + resources / 'kcs.pdf', + outpdf, + '--output-type', + 'pdf', + '--plugin', + 'tests/plugins/tesseract_noop.py', ) p = pikepdf.open(outpdf) - if pikepdf.Name.First in p.root.Outlines: - assert isinstance(p.root.Outlines.First, pikepdf.Dictionary) + if pikepdf.Name.First in p.Root.Outlines: + assert isinstance(p.Root.Outlines.First, pikepdf.Dictionary) +@pytest.mark.skipif( + pikepdf.__version__ in ('2.2.2', '2.2.3'), reason="Raises wrong warning" +) def test_metadata_fixup_warning(resources, outdir, caplog): - from ocrmypdf._pipeline import metadata_fixup - - input_files = [ - str(outdir / 'graph.repaired.pdf'), - str(outdir / 'layers.rendered.pdf'), - str(outdir / 'pdfa.pdf'), # It is okay that this is not a PDF/A - ] - for f in input_files: - copyfile(resources / 'graph.pdf', f) - - log = logging.getLogger() - context = MagicMock() - metadata_fixup( - input_files_groups=input_files, - output_file=outdir / 'out.pdf', - log=log, - context=context, + options = get_parser().parse_args( + args=['--output-type', 'pdfa-2', 'graph.pdf', 'out.pdf'] ) + + copyfile(resources / 'graph.pdf', outdir / 'graph.pdf') + + context = PdfContext( + options, outdir, outdir / 'graph.pdf', None, get_plugin_manager([]) + ) + metadata_fixup(working_file=outdir / 'graph.pdf', context=context) for record in caplog.records: - assert record.levelname != 'WARNING' + assert record.levelname != 'WARNING', "Unexpected warning" # Now add some metadata that will not be copyable - graph = pikepdf.open(outdir / 'graph.repaired.pdf') + graph = pikepdf.open(outdir / 'graph.pdf') with graph.open_metadata() as meta: meta['prism2:publicationName'] = 'OCRmyPDF Test' - graph.save(outdir / 'graph.repaired.pdf') + graph.save(outdir / 'graph_mod.pdf') - log = logging.getLogger() - context = MagicMock() - metadata_fixup( - input_files_groups=input_files, - output_file=outdir / 'out.pdf', - log=log, - context=context, + context = PdfContext( + options, outdir, outdir / 'graph_mod.pdf', None, get_plugin_manager([]) ) + metadata_fixup(working_file=outdir / 'graph.pdf', context=context) assert any(record.levelname == 'WARNING' for record in caplog.records) def test_prevent_gs_invalid_xml(resources, outdir): - from ocrmypdf.__main__ import parser - from ocrmypdf._pipeline import convert_to_pdfa - from ocrmypdf.pdfa import generate_pdfa_ps - from ocrmypdf.pdfinfo import PdfInfo - generate_pdfa_ps(outdir / 'pdfa.ps') - input_files = [str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps')] - copyfile(resources / 'enron1.pdf', outdir / 'layers.rendered.pdf') - log = logging.getLogger() - context = JobContext() + copyfile(resources / 'trivial.pdf', outdir / 'layers.rendered.pdf') - options = parser.parse_args( + # Inject a string with a trailing nul character into the DocumentInfo + # dictionary of this PDF, as often occurs in practice. + with pikepdf.open(outdir / 'layers.rendered.pdf') as pike: + pike.Root.DocumentInfo = pikepdf.Dictionary( + Title=b'String with trailing nul\x00' + ) + + options = get_parser().parse_args( args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf'] ) - context.options = options - context.pdfinfo = PdfInfo(resources / 'enron1.pdf') - - convert_to_pdfa( - input_files_groups=input_files, - output_file=outdir / 'pdfa.pdf', - log=log, - context=context, + pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf') + context = PdfContext( + options, outdir, outdir / 'layers.rendered.pdf', pdfinfo, get_plugin_manager([]) ) - with open(outdir / 'pdfa.pdf', 'rb') as f: - with mmap.mmap( - f.fileno(), 0, flags=mmap.MAP_PRIVATE, prot=mmap.PROT_READ - ) as mm: - # Since the XML may be invalid, we scan instead of actually feeding it - # to a parser. - XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d' - xmp_start = mm.find(XMP_MAGIC) - xmp_end = mm.rfind(b'") + pike.save(outdir / 'layers.rendered.pdf', fix_metadata_version=False) + + options = get_parser().parse_args( + args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf'] + ) + pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf') + context = PdfContext( + options, outdir, outdir / 'layers.rendered.pdf', pdfinfo, get_plugin_manager([]) + ) + + convert_to_pdfa( + str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context + ) + + print(caplog.records) + assert any( + 'malformed DocumentInfo block' in record.message for record in caplog.records + ) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py deleted file mode 100644 index a25457ba..00000000 --- a/tests/test_multiprocessing.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -from multiprocessing import Process -from multiprocessing.managers import BaseProxy - -from ocrmypdf._jobcontext import JobContext, JobContextManager -from ocrmypdf.pdfinfo import PageInfo, PdfInfo - - -def test_jobcontext_proxy(resources): - # Prove that managers are set up correctly to share state among processes - manager = JobContextManager() - manager.register('JobContext', JobContext) - - # Start the manager in a child process (or maybe thread) - manager.start() - - # Tell the manager process to retrieve pdf info - context = manager.JobContext() - context.generate_pdfinfo(resources / 'graph.pdf') - - # Get a copy of that information for this process - pdfinfo = context.get_pdfinfo() - assert len(pdfinfo) == 1 - assert pdfinfo[0].rotation == 0 - - # Update information and send back to manager - pdfinfo[0].rotation = 90 - context.set_pdfinfo(pdfinfo) - - # Retrieve again, ensure it stayed changed - pdfinfo2 = context.get_pdfinfo() - assert pdfinfo2[0].rotation == 90 - - # Start a new process which gets its own proxy object - def client(context): - assert isinstance(context, BaseProxy) - pdfinfo = context.get_pdfinfo() - page = pdfinfo[0] - assert page.rotation == 90 - page.rotation += 90 - context.set_pdfinfo(pdfinfo) - - p = Process(target=client, args=(context,)) - p.start() - p.join() - assert p.exitcode == 0, "Child process failed" - - assert context.get_pdfinfo()[0].rotation == 180 diff --git a/tests/test_optimize.py b/tests/test_optimize.py index 6884ee8e..3c5b3b38 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -1,43 +1,44 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import logging from os import fspath from pathlib import Path +from unittest.mock import patch -import pytest -from PIL import Image - +import img2pdf import pikepdf +import pytest +from PIL import Image, ImageDraw + from ocrmypdf import optimize as opt -from ocrmypdf.exec import jbig2enc, pngquant -from ocrmypdf.exec.ghostscript import rasterize_pdf +from ocrmypdf._exec import jbig2enc, pngquant +from ocrmypdf._exec.ghostscript import rasterize_pdf +from ocrmypdf.helpers import Resolution -check_ocrmypdf = pytest.helpers.check_ocrmypdf # pylint: disable=e1101 +from .conftest import check_ocrmypdf + +needs_pngquant = pytest.mark.skipif( + not pngquant.available(), reason="pngquant not installed" +) +needs_jbig2enc = pytest.mark.skipif( + not jbig2enc.available(), reason="jbig2enc not installed" +) +@needs_pngquant @pytest.mark.parametrize('pdf', ['multipage.pdf', 'palette.pdf']) def test_basic(resources, pdf, outpdf): infile = resources / pdf opt.main(infile, outpdf, level=3) - assert Path(outpdf).stat().st_size <= Path(infile).stat().st_size + assert 0.98 * Path(outpdf).stat().st_size <= Path(infile).stat().st_size +@needs_pngquant def test_mono_not_inverted(resources, outdir): infile = resources / '2400dpi.pdf' opt.main(infile, outdir / 'out.pdf', level=3) @@ -45,17 +46,16 @@ def test_mono_not_inverted(resources, outdir): rasterize_pdf( outdir / 'out.pdf', outdir / 'im.png', - xres=10, - yres=10, raster_device='pnggray', - log=logging.getLogger(name='test_mono_flip'), + raster_dpi=Resolution(10, 10), ) - im = Image.open(fspath(outdir / 'im.png')) - assert im.getpixel((0, 0)) == 255, "Expected white background" + with Image.open(fspath(outdir / 'im.png')) as im: + assert im.getpixel((0, 0)) == 255, "Expected white background" -def test_jpg_png_params(resources, outpdf, spoof_tesseract_noop): +@needs_pngquant +def test_jpg_png_params(resources, outpdf): check_ocrmypdf( resources / 'crom.png', outpdf, @@ -67,13 +67,14 @@ def test_jpg_png_params(resources, outpdf, spoof_tesseract_noop): '50', '--png-quality', '20', - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) -@pytest.mark.skipif(not jbig2enc.available(), reason='need jbig2enc') +@needs_jbig2enc @pytest.mark.parametrize('lossy', [False, True]) -def test_jbig2_lossy(lossy, resources, outpdf, spoof_tesseract_noop): +def test_jbig2_lossy(lossy, resources, outpdf): args = [ resources / 'ccitt.pdf', outpdf, @@ -85,11 +86,13 @@ def test_jbig2_lossy(lossy, resources, outpdf, spoof_tesseract_noop): '50', '--png-quality', '20', + '--plugin', + 'tests/plugins/tesseract_noop.py', ] if lossy: args.append('--jbig2-lossy') - check_ocrmypdf(*args, env=spoof_tesseract_noop) + check_ocrmypdf(*args) pdf = pikepdf.open(outpdf) pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values()))) @@ -101,18 +104,16 @@ def test_jbig2_lossy(lossy, resources, outpdf, spoof_tesseract_noop): assert len(pim.decode_parms) == 0 -@pytest.mark.skipif( - not jbig2enc.available() or not pngquant.available(), - reason='need jbig2enc and pngquant', -) -def test_flate_to_jbig2(resources, outdir, spoof_tesseract_noop): +@needs_pngquant +@needs_jbig2enc +def test_flate_to_jbig2(resources, outdir): # This test requires an image that pngquant is capable of converting to # to 1bpp - so use an existing 1bpp image, convert up, confirm it can # convert down - im = Image.open(fspath(resources / 'typewriter.png')) - assert im.mode in ('1', 'P') - im = im.convert('L') - im.save(fspath(outdir / 'type8.png')) + with Image.open(fspath(resources / 'typewriter.png')) as im: + assert im.mode in ('1', 'P') + im = im.convert('L') + im.save(fspath(outdir / 'type8.png')) check_ocrmypdf( outdir / 'type8.png', @@ -123,9 +124,77 @@ def test_flate_to_jbig2(resources, outdir, spoof_tesseract_noop): '50', '--optimize', '3', - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) pdf = pikepdf.open(outdir / 'out.pdf') pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values()))) assert pim.filters[0] == '/JBIG2Decode' + + +@needs_pngquant +def test_multiple_pngs(resources, outdir): + with Path.open(outdir / 'in.pdf', 'wb') as inpdf: + img2pdf.convert( + fspath(resources / 'baiona_colormapped.png'), + fspath(resources / 'baiona_gray.png'), + with_pdfrw=False, + outputstream=inpdf, + ) + + def mockquant(input_file, output_file, *_args): + with Image.open(input_file) as im: + draw = ImageDraw.Draw(im) + draw.rectangle((0, 0, im.width, im.height), fill=128) + im.save(output_file) + + with patch('ocrmypdf.optimize.pngquant.quantize') as mock: + mock.side_effect = mockquant + check_ocrmypdf( + outdir / 'in.pdf', + outdir / 'out.pdf', + '--optimize', + '3', + '--jobs', + '1', + '--use-threads', + '--output-type', + 'pdf', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + mock.assert_called() + + with pikepdf.open(outdir / 'in.pdf') as inpdf, pikepdf.open( + outdir / 'out.pdf' + ) as outpdf: + for n in range(len(inpdf.pages)): + inim = next(iter(inpdf.pages[n].images.values())) + outim = next(iter(outpdf.pages[n].images.values())) + assert len(outim.read_raw_bytes()) < len(inim.read_raw_bytes()), n + + +def test_optimize_off(resources, outpdf): + check_ocrmypdf( + resources / 'trivial.pdf', + outpdf, + '--optimize=0', + '--output-type', + 'pdf', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + + +def test_group3(resources, outdir): + with pikepdf.open(resources / 'ccitt.pdf') as pdf: + im = pdf.pages[0].Resources.XObject['/Im1'] + assert ( + opt.extract_image_filter(pdf, outdir, im, im.objgen[0]) is not None + ), "Group 4 should be allowed" + + im.DecodeParms['/K'] = 0 + assert ( + opt.extract_image_filter(pdf, outdir, im, im.objgen[0]) is None + ), "Group 3 should be disallowed" diff --git a/tests/test_page_numbers.py b/tests/test_page_numbers.py new file mode 100644 index 00000000..71f06a0d --- /dev/null +++ b/tests/test_page_numbers.py @@ -0,0 +1,70 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import pytest + +import ocrmypdf +from ocrmypdf._validation import _pages_from_ranges +from ocrmypdf.exceptions import BadArgsError +from ocrmypdf.pdfinfo import PdfInfo + + +@pytest.mark.parametrize( + 'pages, result', + [ + ['1', {0}], + ['1,2', {0, 1}], + ['1-3', {0, 1, 2}], + ['2,5,6', {1, 4, 5}], + ['11-15, 18, ', {10, 11, 12, 13, 14, 17}], + [',,3', {2}], + ['3, 3, 3, 3,', {2}], + ['3, 2, 1, 42', {0, 1, 2, 41}], + ['-1', BadArgsError], + ['1,3,-11', BadArgsError], + ['1-,', BadArgsError], + ['start-end', BadArgsError], + ['1-0', BadArgsError], + ['99-98', BadArgsError], + ['0-0', BadArgsError], + ['1-0,3-4', BadArgsError], + [',', BadArgsError], + ['', BadArgsError], + ], +) +def test_pages(pages, result): + if isinstance(result, type): + with pytest.raises(result): + _pages_from_ranges(pages) + else: + assert _pages_from_ranges(pages) == result + + +def test_nonmonotonic_warning(caplog): + pages = _pages_from_ranges('1, 3, 2') + assert pages == {0, 1, 2} + assert 'out of order' in caplog.text + + +def test_list_range(): + assert _pages_from_ranges([0, 1, 2]) == {0, 1, 2} + + +def test_limited_pages(resources, outpdf): + multi = resources / 'multipage.pdf' + ocrmypdf.ocr( + multi, + outpdf, + pages='5-6', + optimize=0, + output_type='pdf', + plugins=['tests/plugins/tesseract_cache.py'], + ) + pi = PdfInfo(outpdf) + assert not pi.pages[0].has_text + assert pi.pages[4].has_text + assert pi.pages[5].has_text diff --git a/tests/test_pdfa.py b/tests/test_pdfa.py new file mode 100644 index 00000000..75b22a57 --- /dev/null +++ b/tests/test_pdfa.py @@ -0,0 +1,34 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import pikepdf +import pytest + +from .conftest import check_ocrmypdf + + +@pytest.mark.parametrize('optimize', (0, 3)) +@pytest.mark.parametrize('pdfa_level', (1, 2, 3)) +def test_pdfa(resources, outpdf, optimize, pdfa_level): + check_ocrmypdf( + resources / 'francais.pdf', + outpdf, + '--plugin', + 'tests/plugins/tesseract_noop.py', + f'--output-type=pdfa-{pdfa_level}', + f'--optimize={optimize}', + ) + if pdfa_level in (2, 3): + # PDF/A-2 allows ObjStm + assert b'/ObjStm' in outpdf.read_bytes() + elif pdfa_level == 1: + # PDF/A-1 might allow ObjStm, but Acrobat does not approve it, so + # we don't use it + assert b'/ObjStm' not in outpdf.read_bytes() + + with pikepdf.open(outpdf) as pdf: + with pdf.open_metadata() as m: + assert m.pdfa_status == f'{pdfa_level}B' diff --git a/tests/test_pdfinfo.py b/tests/test_pdfinfo.py index a4ab14f6..cc93293d 100644 --- a/tests/test_pdfinfo.py +++ b/tests/test_pdfinfo.py @@ -1,42 +1,35 @@ # © 2015 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. import pickle +from io import BytesIO from math import isclose -from tempfile import NamedTemporaryFile import img2pdf +import pikepdf import pytest from PIL import Image +from reportlab.lib.units import inch from reportlab.pdfgen.canvas import Canvas -import pikepdf from ocrmypdf import pdfinfo +from ocrmypdf.exceptions import InputFileError +from ocrmypdf.helpers import Resolution from ocrmypdf.pdfinfo import Colorspace, Encoding +from ocrmypdf.pdfinfo.layout import PDFPage # pylint: disable=protected-access def test_single_page_text(outdir): filename = outdir / 'text.pdf' - pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72)) + pdf = Canvas(str(filename), pagesize=(8 * inch, 6 * inch)) text = pdf.beginText() text.setFont('Helvetica', 12) - text.setTextOrigin(1 * 72, 3 * 72) + text.setTextOrigin(1 * inch, 3 * inch) text.textLine( "Methink'st thou art a general offence and every" " man should beat thee." ) @@ -53,25 +46,32 @@ def test_single_page_text(outdir): assert len(page.images) == 0 -def test_single_page_image(outdir): - filename = outdir / 'image-mono.pdf' - - im_tmp = outdir / 'tmp.png' +@pytest.fixture(scope='session') +def eight_by_eight(): im = Image.new('1', (8, 8), 0) for n in range(8): im.putpixel((n, n), 1) - im.save(str(im_tmp), format='PNG') + return im + + +def test_single_page_image(eight_by_eight, outpdf): + im = eight_by_eight + bio = BytesIO() + im.save(bio, format='PNG') + bio.seek(0) imgsize = ((img2pdf.ImgSize.dpi, 8), (img2pdf.ImgSize.dpi, 8)) layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None) - im_bytes = im_tmp.read_bytes() - pdf_bytes = img2pdf.convert( - im_bytes, producer="img2pdf", with_pdfrw=False, layout_fun=layout_fun - ) - filename.write_bytes(pdf_bytes) - - info = pdfinfo.PdfInfo(filename) + with outpdf.open('wb') as f: + img2pdf.convert( + bio, + producer="img2pdf", + with_pdfrw=False, + layout_fun=layout_fun, + outputstream=f, + ) + info = pdfinfo.PdfInfo(outpdf) assert len(info) == 1 page = info[0] @@ -84,39 +84,35 @@ def test_single_page_image(outdir): assert pdfimage.color == Colorspace.gray # DPI in a 1"x1" is the image width - assert isclose(pdfimage.xres, 8) - assert isclose(pdfimage.yres, 8) + assert isclose(pdfimage.dpi.x, 8) + assert isclose(pdfimage.dpi.y, 8) -def test_single_page_inline_image(outdir): +def test_single_page_inline_image(eight_by_eight, outdir): filename = outdir / 'image-mono-inline.pdf' pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72)) - with NamedTemporaryFile() as im_tmp: - im = Image.new('1', (8, 8), 0) - for n in range(8): - im.putpixel((n, n), 1) - im.save(im_tmp.name, format='PNG') - # Draw image in a 72x72 pt or 1"x1" area - pdf.drawInlineImage(im_tmp.name, 0, 0, width=72, height=72) - pdf.showPage() - pdf.save() - pdf = pdfinfo.PdfInfo(filename) - print(pdf) - pdfimage = pdf[0].images[0] - assert isclose(pdfimage.xres, 8) - assert pdfimage.color == Colorspace.rgb # reportlab produces color image + # Draw image in a 72x72 pt or 1"x1" area + pdf.drawInlineImage(eight_by_eight, 0, 0, width=72, height=72) + pdf.showPage() + pdf.save() + + info = pdfinfo.PdfInfo(filename) + print(info) + pdfimage = info[0].images[0] + assert isclose(pdfimage.dpi.x, 8) + assert pdfimage.color == Colorspace.gray assert pdfimage.width == 8 -def test_jpeg(resources, outdir): +def test_jpeg(resources): filename = resources / 'c02-22.pdf' pdf = pdfinfo.PdfInfo(filename) pdfimage = pdf[0].images[0] assert pdfimage.enc == Encoding.jpeg - assert isclose(pdfimage.xres, 150) + assert isclose(pdfimage.dpi.x, 150) def test_form_xobject(resources): @@ -132,13 +128,13 @@ def test_no_contents(resources): pdf = pdfinfo.PdfInfo(filename) assert len(pdf[0].images) == 0 - assert pdf[0].has_text == False + assert not pdf[0].has_text def test_oversized_page(resources): pdf = pdfinfo.PdfInfo(resources / 'poster.pdf') image = pdf[0].images[0] - assert image.width * image.xres > 200, "this is supposed to be oversized" + assert image.width * image.dpi.x > 200, "this is supposed to be oversized" def test_pickle(resources): @@ -150,22 +146,6 @@ def test_pickle(resources): pickle.dumps(pdf) -def test_regex(): - rx = pdfinfo.ghosttext.regex_remove_char_tags - - must_match = [ - b'', - b'', - b'', - ] - must_not_match = [b'', b'', b'', b''] - - for s in must_match: - assert rx.match(s) - for s in must_not_match: - assert not rx.match(s) - - def test_vector(resources): filename = resources / 'vector.pdf' pdf = pdfinfo.PdfInfo(filename) @@ -183,18 +163,9 @@ def test_ocr_detection(resources): @pytest.mark.parametrize( 'testfile', ('truetype_font_nomapping.pdf', 'type3_font_nomapping.pdf') ) -@pytest.helpers.needs_pdfminer # pylint: disable=e1101 def test_corrupt_font_detection(resources, testfile): - try: - import pdfminer - except ImportError: - pytest.skip("Needs pdfminer") filename = resources / testfile - with pytest.raises(NotImplementedError): - pdf = pdfinfo.PdfInfo(filename) - pdf[0].has_corrupt_text - - pdf = pdfinfo.PdfInfo(filename, detailed_page_analysis=True) + pdf = pdfinfo.PdfInfo(filename, detailed_analysis=True) assert pdf[0].has_corrupt_text @@ -203,15 +174,50 @@ def test_stack_abuse(): stream = pikepdf.Stream(p, b'q ' * 35) with pytest.warns(None) as record: - pdfinfo._interpret_contents(stream) + pdfinfo.info._interpret_contents(stream) assert 'overflowed' in str(record[0].message) stream = pikepdf.Stream(p, b'q Q Q Q Q') with pytest.warns(None) as record: - pdfinfo._interpret_contents(stream) + pdfinfo.info._interpret_contents(stream) assert 'underflowed' in str(record[0].message) stream = pikepdf.Stream(p, b'q ' * 135) with pytest.warns(None): with pytest.raises(RuntimeError): - pdfinfo._interpret_contents(stream) + pdfinfo.info._interpret_contents(stream) + + +def test_pages_issue700(monkeypatch, resources): + def get_no_pages(*args, **kwargs): + return iter([]) + + monkeypatch.setattr(PDFPage, 'get_pages', get_no_pages) + + with pytest.raises(InputFileError, match="pdfminer"): + pdfinfo.PdfInfo( + resources / 'cardinal.pdf', + detailed_analysis=True, + progbar=False, + max_workers=1, + ) + + +def test_image_scale0(resources, outpdf): + with pikepdf.open(resources / 'cmyk.pdf') as cmyk: + xobj = pikepdf.Page(cmyk.pages[0]).as_form_xobject() + + p = pikepdf.Pdf.new() + p.add_blank_page(page_size=(72, 72)) + objname = pikepdf.Page(p.pages[0]).add_resource( + p.copy_foreign(xobj), pikepdf.Name.XObject, pikepdf.Name.Im0 + ) + print(objname) + p.pages[0].Contents = pikepdf.Stream( + p, b"q 0 0 0 0 0 0 cm %s Do Q" % bytes(objname) + ) + p.save(outpdf) + + pi = pdfinfo.PdfInfo(outpdf, detailed_analysis=True, progbar=False, max_workers=1) + assert not pi.pages[0]._images[0].dpi.is_finite + assert pi.pages[0].dpi == Resolution(0, 0) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 00000000..becef2c4 --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,150 @@ +# © 2021 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from unittest.mock import Mock + +import pytest +from PIL import Image +from reportlab.lib.units import inch +from reportlab.lib.utils import ImageReader +from reportlab.pdfgen.canvas import Canvas + +from ocrmypdf import _pipeline, pdfinfo +from ocrmypdf.helpers import Resolution + + +@pytest.fixture(scope='session') +def rgb_image(): + im = Image.new('RGB', (8, 8)) + im.putpixel((4, 4), (255, 0, 0)) + im.putpixel((5, 5), (0, 255, 0)) + im.putpixel((6, 6), (0, 0, 255)) + return ImageReader(im) + + +DUMMY_OVERSAMPLE_RESOLUTION = Resolution(42.0, 42.0) +VECTOR_RESOLUTION = Resolution(_pipeline.VECTOR_PAGE_DPI, _pipeline.VECTOR_PAGE_DPI) + + +@pytest.mark.parametrize( + 'image, text, vector, result', + [ + (False, False, False, VECTOR_RESOLUTION), + (False, True, False, VECTOR_RESOLUTION), + (True, False, False, DUMMY_OVERSAMPLE_RESOLUTION), + (True, True, False, VECTOR_RESOLUTION), + (False, False, True, VECTOR_RESOLUTION), + (False, True, True, VECTOR_RESOLUTION), + (True, False, True, VECTOR_RESOLUTION), + (True, True, True, VECTOR_RESOLUTION), + ], +) +def test_dpi_needed(image, text, vector, result, rgb_image, outdir): + + c = Canvas(str(outdir / 'dpi.pdf'), pagesize=(5 * inch, 5 * inch)) + if image: + c.drawImage(rgb_image, 1 * inch, 1 * inch, width=1 * inch, height=1 * inch) + if text: + c.drawString(1 * inch, 4 * inch, "Actual text") + if vector: + c.ellipse(3 * inch, 3 * inch, 4 * inch, 4 * inch) + c.showPage() + c.save() + + mock = Mock() + mock.oversample = DUMMY_OVERSAMPLE_RESOLUTION[0] + + pi = pdfinfo.PdfInfo(outdir / 'dpi.pdf') + + assert _pipeline.get_canvas_square_dpi(pi[0], mock) == result + assert _pipeline.get_page_square_dpi(pi[0], mock) == result + + +@pytest.mark.parametrize( + # Name for nicer -v output + 'name,input,output', + ( + ( + 'empty_input', + # Input: + (), + # Output: + (), + ), + ( + 'no_values', + # Input: + ('', '', '', '', ''), + # Output: + ( + ((1, 5), None), + ), + ), + ( + 'no_empty_values', + # Input: + ('v', 'w', 'x', 'y', 'z'), + # Output: + ( + ((1, 1), 'v'), + ((2, 2), 'w'), + ((3, 3), 'x'), + ((4, 4), 'y'), + ((5, 5), 'z'), + ), + ), + ( + 'skip_head', + # Input: + ('', '', 'x', 'y', 'z'), + # Output: + ( + ((1, 2), None), + ((3, 3), 'x'), + ((4, 4), 'y'), + ((5, 5), 'z'), + ), + ), + ( + 'skip_tail', + # Input: + ('x', 'y', 'z', '', ''), + # Output: + ( + ((1, 1), 'x'), + ((2, 2), 'y'), + ((3, 3), 'z'), + ((4, 5), None), + ), + ), + ( + 'range_in_middle', + # Input: + ('x', '', '', '', 'y'), + # Output: + ( + ((1, 1), 'x'), + ((2, 4), None), + ((5, 5), 'y'), + ), + ), + ( + 'range_in_middle_2', + # Input: + ('x', '', '', 'y', '', '', '', 'z'), + # Output: + ( + ((1, 1), 'x'), + ((2, 3), None), + ((4, 4), 'y'), + ((5, 7), None), + ((8, 8), 'z'), + ), + ), + ), +) +def test_enumerate_compress_ranges(name, input, output): + assert output == tuple(_pipeline.enumerate_compress_ranges(input)) \ No newline at end of file diff --git a/tests/test_preprocessing.py b/tests/test_preprocessing.py new file mode 100644 index 00000000..fa6bc687 --- /dev/null +++ b/tests/test_preprocessing.py @@ -0,0 +1,163 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +from math import isclose + +import pytest +from PIL import Image + +from ocrmypdf._exec import ghostscript +from ocrmypdf.helpers import Resolution +from ocrmypdf.leptonica import Pix +from ocrmypdf.pdfinfo import PdfInfo + +from .conftest import check_ocrmypdf, have_unpaper + +RENDERERS = ['hocr', 'sandwich'] + + +def test_deskew(resources, outdir): + # Run with deskew + deskewed_pdf = check_ocrmypdf( + resources / 'skew.pdf', + outdir / 'skew.pdf', + '-d', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + + # Now render as an image again and use Leptonica to find the skew angle + # to confirm that it was deskewed + deskewed_png = outdir / 'deskewed.png' + + ghostscript.rasterize_pdf( + deskewed_pdf, + deskewed_png, + raster_device='pngmono', + raster_dpi=Resolution(150, 150), + pageno=1, + ) + + pix = Pix.open(deskewed_png) + skew_angle, _skew_confidence = pix.find_skew() + + print(skew_angle) + assert -0.5 < skew_angle < 0.5, "Deskewing failed" + + +def test_remove_background(resources, outdir): + # Ensure the input image does not contain pure white/black + with Image.open(resources / 'congress.jpg') as im: + assert im.getextrema() != ((0, 255), (0, 255), (0, 255)) + + output_pdf = check_ocrmypdf( + resources / 'congress.jpg', + outdir / 'test_remove_bg.pdf', + '--remove-background', + '--image-dpi', + '150', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + + output_png = outdir / 'remove_bg.png' + + ghostscript.rasterize_pdf( + output_pdf, + output_png, + raster_device='png16m', + raster_dpi=Resolution(100, 100), + pageno=1, + ) + + # The output image should contain pure white and black + with Image.open(output_png) as im: + assert im.getextrema() == ((0, 255), (0, 255), (0, 255)) + + +# This will run 5 * 2 * 2 = 20 test cases +@pytest.mark.parametrize( + "pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'] +) +@pytest.mark.parametrize("renderer", ['sandwich', 'hocr']) +@pytest.mark.parametrize("output_type", ['pdf', 'pdfa']) +def test_exotic_image(pdf, renderer, output_type, resources, outdir): + outfile = outdir / f'test_{pdf}_{renderer}.pdf' + check_ocrmypdf( + resources / pdf, + outfile, + '-dc' if have_unpaper() else '-d', + '-v', + '1', + '--output-type', + output_type, + '--sidecar', + '--skip-text', + '--pdf-renderer', + renderer, + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) + + assert outfile.with_suffix('.pdf.txt').exists() + + +@pytest.mark.parametrize('renderer', RENDERERS) +def test_non_square_resolution(renderer, resources, outpdf): + # Confirm input image is non-square resolution + in_pageinfo = PdfInfo(resources / 'aspect.pdf') + assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y + + check_ocrmypdf( + resources / 'aspect.pdf', + outpdf, + '--pdf-renderer', + renderer, + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) + + out_pageinfo = PdfInfo(outpdf) + + # Confirm resolution was kept the same + assert in_pageinfo[0].dpi == out_pageinfo[0].dpi + + +@pytest.mark.parametrize('renderer', RENDERERS) +def test_convert_to_square_resolution(renderer, resources, outpdf): + # Confirm input image is non-square resolution + in_pageinfo = PdfInfo(resources / 'aspect.pdf') + assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y + + # --force-ocr requires means forced conversion to square resolution + check_ocrmypdf( + resources / 'aspect.pdf', + outpdf, + '--force-ocr', + '--pdf-renderer', + renderer, + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) + + out_pageinfo = PdfInfo(outpdf) + + in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0] + + # Resolution show now be equal + assert out_p0.dpi.x == out_p0.dpi.y + + # Page size should match input page size + assert isclose(in_p0.width_inches, out_p0.width_inches) + assert isclose(in_p0.height_inches, out_p0.height_inches) + + # Because we rasterized the page to produce a new image, it should occupy + # the entire page + out_im_w = out_p0.images[0].width / out_p0.images[0].dpi.x + out_im_h = out_p0.images[0].height / out_p0.images[0].dpi.y + assert isclose(out_p0.width_inches, out_im_w) + assert isclose(out_p0.height_inches, out_im_h) diff --git a/tests/test_qpdf.py b/tests/test_qpdf.py deleted file mode 100644 index 0e925249..00000000 --- a/tests/test_qpdf.py +++ /dev/null @@ -1,25 +0,0 @@ -# © 2018 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import pytest - -import ocrmypdf.exec.qpdf as qpdf - - -def test_qpdf_error(resources): - assert qpdf.check(resources / 'blank.pdf') - assert not qpdf.check(__file__) diff --git a/tests/test_quality.py b/tests/test_quality.py new file mode 100644 index 00000000..132eef3d --- /dev/null +++ b/tests/test_quality.py @@ -0,0 +1,25 @@ +# © 2020 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import pytest + +from ocrmypdf import quality as qual + + +def test_quality_measurement(): + oqd = qual.OcrQualityDictionary( + wordlist=["words", "words", "quick", "brown", "fox", "dog", "lazy"] + ) + assert len(oqd.dictionary) == 6 # 6 unique + + assert ( + oqd.measure_words_matched("The quick brown fox jumps quickly over the lazy dog") + == 0.5 + ) + assert oqd.measure_words_matched("12345 10% _f 7fox -brown | words") == 1.0 + + assert oqd.measure_words_matched("quick quick quick") == 1.0 diff --git a/tests/test_rotation.py b/tests/test_rotation.py index bfcf3dae..3639eec7 100644 --- a/tests/test_rotation.py +++ b/tests/test_rotation.py @@ -1,46 +1,36 @@ # © 2018 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import logging from io import BytesIO +from math import cos, pi, sin from os import fspath -from unittest.mock import Mock import img2pdf +import pikepdf import pytest from PIL import Image +from reportlab.pdfgen.canvas import Canvas -import pikepdf from ocrmypdf import leptonica -from ocrmypdf.exec import ghostscript, tesseract +from ocrmypdf._exec import ghostscript +from ocrmypdf._plugin_manager import get_plugin_manager +from ocrmypdf.helpers import Resolution from ocrmypdf.pdfinfo import PdfInfo -# pytest.helpers is dynamic -# pylint: disable=no-member -# pylint: disable=w0612 +from .conftest import check_ocrmypdf, run_ocrmypdf + +# pylintx: disable=unused-variable + pytestmark = pytest.mark.skipif( leptonica.get_leptonica_version() < 'leptonica-1.72', reason="Leptonica is too old, correlation doesn't work", ) -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf - RENDERERS = ['hocr', 'sandwich'] @@ -48,8 +38,6 @@ RENDERERS = ['hocr', 'sandwich'] def check_monochrome_correlation( outdir, reference_pdf, reference_pageno, test_pdf, test_pageno ): - gslog = logging.getLogger() - reference_png = outdir / f'{reference_pdf.name}.ref{reference_pageno:04d}.png' test_png = outdir / f'{test_pdf.name}.test{test_pageno:04d}.png' @@ -60,10 +48,8 @@ def check_monochrome_correlation( ghostscript.rasterize_pdf( pdf, png, - xres=100, - yres=100, raster_device='pngmono', - log=gslog, + raster_dpi=Resolution(100, 100), pageno=pageno, rotation=0, ) @@ -100,10 +86,10 @@ def test_monochrome_correlation(resources, outdir): @pytest.mark.slow @pytest.mark.parametrize('renderer', RENDERERS) -def test_autorotate(spoof_tesseract_cache, renderer, resources, outdir): +def test_autorotate(renderer, resources, outdir): # cardinal.pdf contains four copies of an image rotated in each cardinal # direction - these ones are "burned in" not tagged with /Rotate - out = check_ocrmypdf( + check_ocrmypdf( resources / 'cardinal.pdf', outdir / 'out.pdf', '-r', @@ -111,7 +97,8 @@ def test_autorotate(spoof_tesseract_cache, renderer, resources, outdir): '1', '--pdf-renderer', renderer, - env=spoof_tesseract_cache, + '--plugin', + 'tests/plugins/tesseract_cache.py', ) for n in range(1, 4 + 1): correlation = check_monochrome_correlation( @@ -131,28 +118,27 @@ def test_autorotate(spoof_tesseract_cache, renderer, resources, outdir): ('99', 'correlation < 0.10'), # High thres -> never rotate -> low corr ], ) -def test_autorotate_threshold( - spoof_tesseract_cache, threshold, correlation_test, resources, outdir -): - out = check_ocrmypdf( +def test_autorotate_threshold(threshold, correlation_test, resources, outdir): + check_ocrmypdf( resources / 'cardinal.pdf', outdir / 'out.pdf', '--rotate-pages-threshold', threshold, '-r', - '-v', - '1', - env=spoof_tesseract_cache, + # '-v', + # '1', + '--plugin', + 'tests/plugins/tesseract_cache.py', ) - correlation = check_monochrome_correlation( + correlation = check_monochrome_correlation( # pylint: disable=unused-variable outdir, reference_pdf=resources / 'cardinal.pdf', reference_pageno=1, test_pdf=outdir / 'out.pdf', test_pageno=3, ) - assert eval(correlation_test) # pylint: disable=w0123 + assert eval(correlation_test) # pylint: disable=eval-used def test_rotated_skew_timeout(resources, outpdf): @@ -224,12 +210,12 @@ def test_rotate_deskew_timeout(resources, outdir): @pytest.mark.parametrize('image_angle', (0, 90, 180, 270)) def test_rotate_page_level(image_angle, page_angle, resources, outdir): def make_rotate_test(prefix, image_angle, page_angle): - im = Image.open(fspath(resources / 'typewriter.png')) - if image_angle != 0: - ccw_angle = -image_angle % 360 - im = im.transpose(getattr(Image, f'ROTATE_{ccw_angle}')) memimg = BytesIO() - im.save(memimg, format='PNG') + with Image.open(fspath(resources / 'typewriter.png')) as im: + if image_angle != 0: + ccw_angle = -image_angle % 360 + im = im.transpose(getattr(Image, f'ROTATE_{ccw_angle}')) + im.save(memimg, format='PNG') memimg.seek(0) mempdf = BytesIO() img2pdf.convert( @@ -255,7 +241,7 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir): '--rotate-pages', '--rotate-pages-threshold', '0.001', - universal_newlines=False, + text=False, ) err = err.decode('utf-8', errors='replace') assert p.returncode == 0, err @@ -263,12 +249,78 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir): assert check_monochrome_correlation(outdir, reference, 1, out, 1) > 0.2 -def test_tesseract_orientation(resources, tmpdir): - pix = leptonica.Pix.open(resources / 'crom.png') - pix_rotated = pix.rotate_orth(2) # 180 degrees clockwise - pix_rotated.write_implied_format(tmpdir / '000001.png') +def test_rasterize_rotates(resources, tmp_path): + pm = get_plugin_manager([]) - log = Mock() - tesseract.get_orientation( # Test results of this are unreliable - tmpdir / '000001.png', engine_mode='3', timeout=10, log=log + img = tmp_path / 'img90.png' + pm.hook.rasterize_pdf_page( + input_file=resources / 'graph.pdf', + output_file=img, + raster_device='pngmono', + raster_dpi=Resolution(20, 20), + page_dpi=Resolution(20, 20), + pageno=1, + rotation=90, + filter_vector=False, ) + assert Image.open(img).size == (123, 151), "Image not rotated" + + img = tmp_path / 'img180.png' + pm.hook.rasterize_pdf_page( + input_file=resources / 'graph.pdf', + output_file=img, + raster_device='pngmono', + raster_dpi=Resolution(20, 20), + page_dpi=Resolution(20, 20), + pageno=1, + rotation=180, + filter_vector=False, + ) + assert Image.open(img).size == (151, 123), "Image not rotated" + + +def test_simulated_scan(outdir): + canvas = Canvas( + fspath(outdir / 'fakescan.pdf'), + pagesize=(209.8, 297.6), + ) + + page_vars = [(2, 36, 250), (91, 170, 240), (179, 190, 36), (271, 36, 36)] + + for n, page_var in enumerate(page_vars): + text = canvas.beginText() + text.setFont('Helvetica', 20) + + angle, x, y = page_var + cos_a, sin_a = cos(angle / 180.0 * pi), sin(angle / 180.0 * pi) + + text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, x, y) + text.textOut(f'Page {n + 1}') + canvas.drawText(text) + canvas.showPage() + canvas.save() + + check_ocrmypdf( + outdir / 'fakescan.pdf', + outdir / 'out.pdf', + '--force-ocr', + '--deskew', + '--rotate-pages', + '--plugin', + 'tests/plugins/tesseract_debug_rotate.py', + ) + + with pikepdf.open(outdir / 'out.pdf') as pdf: + assert ( + pdf.pages[1].MediaBox[2] > pdf.pages[1].MediaBox[3] + ), "Wrong orientation: not landscape" + assert ( + pdf.pages[3].MediaBox[2] > pdf.pages[3].MediaBox[3] + ), "Wrong orientation: Not landscape" + + assert ( + pdf.pages[0].MediaBox[2] < pdf.pages[0].MediaBox[3] + ), "Wrong orientation: Not portrait" + assert ( + pdf.pages[2].MediaBox[2] < pdf.pages[2].MediaBox[3] + ), "Wrong orientation: Not portrait" diff --git a/tests/test_stdio.py b/tests/test_stdio.py new file mode 100644 index 00000000..f5993704 --- /dev/null +++ b/tests/test_stdio.py @@ -0,0 +1,116 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os +import sys +from pathlib import Path +from subprocess import DEVNULL, PIPE, Popen, run + +import pytest + +from ocrmypdf.exceptions import ExitCode +from ocrmypdf.helpers import check_pdf + +from .conftest import run_ocrmypdf + + +def test_stdin(ocrmypdf_exec, resources, outpdf): + input_file = str(resources / 'francais.pdf') + output_file = str(outpdf) + + # Runs: ocrmypdf - output.pdf < testfile.pdf + with open(input_file, 'rb') as input_stream: + p_args = ocrmypdf_exec + [ + '-', + output_file, + '--plugin', + 'tests/plugins/tesseract_noop.py', + ] + run(p_args, stdout=PIPE, stderr=PIPE, stdin=input_stream, check=True) + + +def test_stdout(ocrmypdf_exec, resources, outpdf): + if 'COV_CORE_DATAFILE' in os.environ: + pytest.skip(msg="Coverage uses stdout") + + input_file = str(resources / 'francais.pdf') + output_file = str(outpdf) + + # Runs: ocrmypdf francais.pdf - > test_stdout.pdf + with open(output_file, 'wb') as output_stream: + p_args = ocrmypdf_exec + [ + input_file, + '-', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ] + run(p_args, stdout=output_stream, stderr=PIPE, stdin=DEVNULL, check=True) + + assert check_pdf(output_file) + + +@pytest.mark.skipif( + sys.version_info[0:3] >= (3, 6, 4), reason="issue fixed in Python 3.6.4" +) +@pytest.mark.skipif(os.name == 'nt', reason="POSIX problem") +def test_closed_streams(ocrmypdf_exec, resources, outpdf): + input_file = str(resources / 'francais.pdf') + output_file = str(outpdf) + + def evil_closer(): + os.close(0) + os.close(1) + + p_args = ocrmypdf_exec + [ + input_file, + output_file, + '--plugin', + 'tests/plugins/tesseract_noop.py', + ] + p = Popen( # pylint: disable=subprocess-popen-preexec-fn + p_args, + close_fds=True, + stdout=None, + stderr=PIPE, + stdin=None, + preexec_fn=evil_closer, + ) + _out, err = p.communicate() + print(err.decode()) + assert p.returncode == ExitCode.ok + + +@pytest.mark.skipif(sys.version_info >= (3, 7, 0), reason='better utf-8') +@pytest.mark.skipif( + Path('/etc/alpine-release').exists(), reason="invalid test on alpine" +) +@pytest.mark.skipif(os.name == 'nt', reason="invalid test on Windows") +def test_bad_locale(monkeypatch): + monkeypatch.setenv('LC_ALL', 'C') + p, out, err = run_ocrmypdf('a', 'b') + assert out == '', "stdout not clean" + assert p.returncode != 0 + assert 'configured to use ASCII as encoding' in err, "should whine" + + +@pytest.mark.xfail( + os.name == 'nt' and sys.version_info < (3, 8), + reason="Windows does not like this; not sure how to fix", +) +def test_dev_null(resources): + if 'COV_CORE_DATAFILE' in os.environ: + pytest.skip(msg="Coverage uses stdout") + + p, out, _err = run_ocrmypdf( + resources / 'trivial.pdf', + os.devnull, + '--force-ocr', + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + assert p.returncode == 0, "could not send output to /dev/null" + assert len(out) == 0, "wrote to stdout" diff --git a/tests/test_tess4.py b/tests/test_tess4.py deleted file mode 100644 index d4330b83..00000000 --- a/tests/test_tess4.py +++ /dev/null @@ -1,185 +0,0 @@ -# © 2017 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import os -from contextlib import contextmanager -from os import fspath -from pathlib import Path - -import pytest - -from ocrmypdf import pdfinfo -from ocrmypdf.exceptions import MissingDependencyError -from ocrmypdf.exec import tesseract - -# pylint: disable=no-member,w0621 -spoof = pytest.helpers.spoof - - -def _ensure_tess4(): - if tesseract.v4(): - # "tesseract" on $PATH is already v4 - return os.environ.copy() - - if os.environ.get('OCRMYPDF_TESS4'): - # OCRMYPDF_TESS4 is a hint environment variable that tells us to look - # somewhere special for tess4 if and only if we need it. This allows - # setting OCRMYPDF_TESS4 to test tess4 and PATH to point to tess3 - # on a system with both installed. - env = os.environ.copy() - tess4 = Path(os.environ['OCRMYPDF_TESS4']) - assert tess4.is_file() - env['PATH'] = tess4.parent + ':' + env['PATH'] - env['OCRMYPDF_TESS4'] = os.environ['OCRMYPDF_TESS4'] - return env - - raise EnvironmentError("Can't find Tesseract 4") - - -@pytest.fixture -def ensure_tess4(): - return _ensure_tess4() - - -@contextmanager -def modified_os_environ(env): - old_env = os.environ.copy() - os.environ.update(env) - yield - for key in env: - del os.environ[key] - if key in old_env: - os.environ[key] = old_env[key] - - -def tess4_available(): - """Check if a tesseract 4 binary is available, even if it's not the - official "tesseract" on PATH - - """ - try: - # _ensure_tess4 locates the tess4 binary we are going to check - env = _ensure_tess4() - with modified_os_environ(env): - # Now jump into this environment and make sure it really is Tess4 - return tesseract.v4() and tesseract.has_textonly_pdf() - except EnvironmentError: - pass - - return False - - -# Skip all tests in this file if not tesseract 4 -pytestmark = pytest.mark.skipif( - not tess4_available(), reason="tesseract 4.0 with textonly_pdf feature required" -) - -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof - - -def test_textonly_pdf(ensure_tess4, resources, outdir): - check_ocrmypdf( - resources / 'linn.pdf', - outdir / 'linn_textonly.pdf', - '--pdf-renderer', - 'sandwich', - '--sidecar', - outdir / 'foo.txt', - env=ensure_tess4, - ) - - -def test_pagesize_consistency_tess4(ensure_tess4, resources, outpdf): - from math import isclose - - infile = resources / 'linn.pdf' - - before_dims = pytest.helpers.first_page_dimensions(infile) - - check_ocrmypdf( - infile, - outpdf, - '--pdf-renderer', - 'sandwich', - '--clean' if pytest.helpers.have_unpaper() else None, - '--deskew', - '--remove-background', - '--clean-final' if pytest.helpers.have_unpaper() else None, - env=ensure_tess4, - ) - - after_dims = pytest.helpers.first_page_dimensions(outpdf) - - assert isclose(before_dims[0], after_dims[0]) - assert isclose(before_dims[1], after_dims[1]) - - -@pytest.mark.parametrize('basename', ['graph_ocred.pdf', 'cardinal.pdf']) -def test_skip_pages_does_not_replicate(ensure_tess4, resources, basename, outdir): - infile = resources / basename - outpdf = outdir / basename - - check_ocrmypdf( - infile, - outpdf, - '--pdf-renderer', - 'sandwich', - '--force-ocr', - '--tesseract-timeout', - '0', - env=ensure_tess4, - ) - - info_in = pdfinfo.PdfInfo(infile) - - info = pdfinfo.PdfInfo(outpdf) - for page in info: - assert len(page.images) == 1, "skipped page was replicated" - - for n in range(len(info_in)): - assert info[n].width_inches == info_in[n].width_inches - - -def test_content_preservation(ensure_tess4, resources, outpdf): - infile = resources / 'masks.pdf' - - check_ocrmypdf( - infile, - outpdf, - '--pdf-renderer', - 'sandwich', - '--tesseract-timeout', - '0', - env=ensure_tess4, - ) - - info = pdfinfo.PdfInfo(outpdf) - page = info[0] - assert len(page.images) > 1, "masks were rasterized" - - -def test_no_languages(ensure_tess4, tmpdir): - env = ensure_tess4 - (tmpdir / 'tessdata').mkdir() - env['TESSDATA_PREFIX'] = fspath(tmpdir) - - with modified_os_environ(env): - with pytest.raises(MissingDependencyError): - tesseract.languages.cache_clear() - tesseract.languages() diff --git a/tests/test_tesseract.py b/tests/test_tesseract.py new file mode 100644 index 00000000..f9223c48 --- /dev/null +++ b/tests/test_tesseract.py @@ -0,0 +1,145 @@ +# © 2017 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +import os +import subprocess +from os import fspath +from pathlib import Path + +import pytest + +from ocrmypdf import pdfinfo +from ocrmypdf._exec import tesseract +from ocrmypdf.exceptions import MissingDependencyError + +from .conftest import check_ocrmypdf + +# pylint: disable=redefined-outer-name + + +@pytest.mark.parametrize('basename', ['graph_ocred.pdf', 'cardinal.pdf']) +def test_skip_pages_does_not_replicate(resources, basename, outdir): + infile = resources / basename + outpdf = outdir / basename + + check_ocrmypdf( + infile, + outpdf, + '--pdf-renderer', + 'sandwich', + '--force-ocr', + '--tesseract-timeout', + '0', + ) + + info_in = pdfinfo.PdfInfo(infile) + + info = pdfinfo.PdfInfo(outpdf) + for page in info: + assert len(page.images) == 1, "skipped page was replicated" + + for n, info_out_n in enumerate(info): + assert info_out_n.width_inches == info_in[n].width_inches, "output resized" + assert info_out_n.height_inches == info_in[n].height_inches, "output resized" + + +def test_content_preservation(resources, outpdf): + infile = resources / 'masks.pdf' + + check_ocrmypdf( + infile, outpdf, '--pdf-renderer', 'sandwich', '--tesseract-timeout', '0' + ) + + info = pdfinfo.PdfInfo(outpdf) + page = info[0] + assert len(page.images) > 1, "masks were rasterized" + + +def test_no_languages(tmp_path, monkeypatch): + (tmp_path / 'tessdata').mkdir() + monkeypatch.setenv('TESSDATA_PREFIX', fspath(tmp_path)) + with pytest.raises(MissingDependencyError): + tesseract.get_languages() + + +def test_image_too_large_hocr(monkeypatch, resources, outdir): + def dummy_run(args, *, env=None, **kwargs): + raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large') + + monkeypatch.setattr(tesseract, 'run', dummy_run) + tesseract.generate_hocr( + input_file=resources / 'crom.png', + output_hocr=outdir / 'out.hocr', + output_text=outdir / 'out.txt', + languages=['eng'], + engine_mode=None, + tessconfig=[], + timeout=180.0, + pagesegmode=None, + user_words=None, + user_patterns=None, + ) + assert "name='ocr-capabilities'" in Path(outdir / 'out.hocr').read_text() + + +def test_image_too_large_pdf(monkeypatch, resources, outdir): + def dummy_run(args, *, env=None, **kwargs): + raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large') + + monkeypatch.setattr(tesseract, 'run', dummy_run) + tesseract.generate_pdf( + input_file=resources / 'crom.png', + output_pdf=outdir / 'pdf.pdf', + output_text=outdir / 'txt.txt', + languages=['eng'], + engine_mode=None, + tessconfig=[], + timeout=180.0, + pagesegmode=None, + user_words=None, + user_patterns=None, + ) + assert Path(outdir / 'txt.txt').read_text() == '[skipped page]' + if os.name != 'nt': # different semantics + assert Path(outdir / 'pdf.pdf').stat().st_size == 0 + + +def test_timeout(caplog): + tesseract.page_timedout(5) + assert "took too long" in caplog.text + + +@pytest.mark.parametrize( + 'in_, logged', + [ + (b'Tesseract Open Source', ''), + (b'lots of diacritics blah blah', 'diacritics'), + (b'Warning in pixReadMem', ''), + (b'OSD: Weak margin', 'unsure about page orientation'), + (b'Error in pixScanForForeground', ''), + (b'Error in boxClipToRectangle', ''), + (b'an unexpected error', 'an unexpected error'), + (b'a dire warning', 'a dire warning'), + (b'read_params_file something', 'read_params_file'), + (b'an innocent message', 'innocent'), + (b'\x7f\x7f\x80innocent unicode failure', 'innocent'), + ], +) +def test_tesseract_log_output(caplog, in_, logged): + caplog.set_level(logging.INFO) + tesseract.tesseract_log_output(in_) + if logged == '': + assert caplog.text == '' + else: + assert logged in caplog.text + + +def test_tesseract_log_output_raises(caplog): + with pytest.raises(tesseract.TesseractConfigError): + tesseract.tesseract_log_output(b'parameter not found: moo') + assert 'not found' in caplog.text diff --git a/tests/test_unpaper.py b/tests/test_unpaper.py index 3133f63c..de5a7f1f 100644 --- a/tests/test_unpaper.py +++ b/tests/test_unpaper.py @@ -1,113 +1,99 @@ # © 2015-17 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + -import argparse -import logging from os import fspath -from pathlib import Path from unittest.mock import patch import pytest -from ocrmypdf import __main__ as main -from ocrmypdf.exceptions import ExitCode -from ocrmypdf.exec import unpaper +from ocrmypdf._plugin_manager import get_parser_options_plugins +from ocrmypdf._validation import check_options +from ocrmypdf.exceptions import ExitCode, MissingDependencyError -# pytest.helpers is dynamic -# pylint: disable=no-member -# pylint: disable=w0612 +from .conftest import check_ocrmypdf, have_unpaper, run_ocrmypdf -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof - - -def have_unpaper(): - try: - unpaper.version() - except Exception: - return False - else: - return True - - -@pytest.fixture(scope="session") -def spoof_unpaper_oldversion(tmpdir_factory): - return spoof(tmpdir_factory, unpaper="unpaper_oldversion.py") +# pylint: disable=redefined-outer-name def test_no_unpaper(resources, no_outpdf): input_ = fspath(resources / "c02-22.pdf") output = fspath(no_outpdf) - options = main.parser.parse_args(args=["--clean", input_, output]) - with patch("ocrmypdf.exec.unpaper.version") as mock_unpaper_version: - mock_unpaper_version.side_effect = FileNotFoundError("unpaper") - with pytest.raises(SystemExit): - main.check_options(options, log=logging.getLogger()) + _parser, options, pm = get_parser_options_plugins(["--clean", input_, output]) + with patch("ocrmypdf._exec.unpaper.version") as mock: + mock.side_effect = FileNotFoundError("unpaper") + + with pytest.raises(MissingDependencyError): + check_options(options, pm) + mock.assert_called() -def test_old_unpaper(spoof_unpaper_oldversion, resources, no_outpdf): - p, out, err = run_ocrmypdf( - resources / "c02-22.pdf", no_outpdf, "--clean", env=spoof_unpaper_oldversion +def test_old_unpaper(resources, no_outpdf): + input_ = fspath(resources / "c02-22.pdf") + output = fspath(no_outpdf) + + _parser, options, pm = get_parser_options_plugins(["--clean", input_, output]) + with patch("ocrmypdf._exec.unpaper.version") as mock: + mock.return_value = '0.5' + + with pytest.raises(MissingDependencyError): + check_options(options, pm) + mock.assert_called() + + +@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper") +def test_clean(resources, outpdf): + check_ocrmypdf( + resources / "skew.pdf", + outpdf, + "-c", + '--plugin', + 'tests/plugins/tesseract_noop.py', ) - assert p.returncode == ExitCode.missing_dependency @pytest.mark.skipif(not have_unpaper(), reason="requires unpaper") -def test_clean(spoof_tesseract_noop, resources, outpdf): - check_ocrmypdf(resources / "skew.pdf", outpdf, "-c", env=spoof_tesseract_noop) - - -@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper") -def test_unpaper_args_valid(spoof_tesseract_noop, resources, outpdf): +def test_unpaper_args_valid(resources, outpdf): check_ocrmypdf( resources / "skew.pdf", outpdf, "-c", "--unpaper-args", "--layout double", # Spaces required here - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) @pytest.mark.skipif(not have_unpaper(), reason="requires unpaper") -def test_unpaper_args_invalid_filename(spoof_tesseract_noop, resources, outpdf): - p, out, err = run_ocrmypdf( +def test_unpaper_args_invalid_filename(resources, outpdf): + p, _out, err = run_ocrmypdf( resources / "skew.pdf", outpdf, "-c", "--unpaper-args", "/etc/passwd", - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) assert "No filenames allowed" in err assert p.returncode == ExitCode.bad_args @pytest.mark.skipif(not have_unpaper(), reason="requires unpaper") -def test_unpaper_args_invalid(spoof_tesseract_noop, resources, outpdf): - p, out, err = run_ocrmypdf( +def test_unpaper_args_invalid(resources, outpdf): + p, _out, _err = run_ocrmypdf( resources / "skew.pdf", outpdf, "-c", "--unpaper-args", "unpaper is not going to like these arguments", - env=spoof_tesseract_noop, + '--plugin', + 'tests/plugins/tesseract_noop.py', ) # Can't tell difference between unpaper choking on bad arguments or some # other unpaper failure diff --git a/tests/test_userunit.py b/tests/test_userunit.py index 81282431..14ac653c 100644 --- a/tests/test_userunit.py +++ b/tests/test_userunit.py @@ -1,19 +1,9 @@ # © 2017 James R. Barlow: github.com/jbarlow83 # -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + from math import isclose @@ -22,9 +12,9 @@ import pytest from ocrmypdf.exceptions import ExitCode from ocrmypdf.pdfinfo import PdfInfo -check_ocrmypdf = pytest.helpers.check_ocrmypdf -run_ocrmypdf = pytest.helpers.run_ocrmypdf -spoof = pytest.helpers.spoof +from .conftest import check_ocrmypdf, run_ocrmypdf_api + +# pylint: disable=redefined-outer-name @pytest.fixture @@ -32,20 +22,32 @@ def poster(resources): return resources / 'poster.pdf' -def test_userunit_ghostscript_fails(poster, no_outpdf): - p, out, err = run_ocrmypdf(poster, no_outpdf, '--output-type=pdfa') - assert p.returncode == ExitCode.input_file +def test_userunit_ghostscript_fails(poster, no_outpdf, caplog): + result = run_ocrmypdf_api(poster, no_outpdf, '--output-type=pdfa') + assert result == ExitCode.input_file + assert 'not supported by Ghostscript' in caplog.text -def test_userunit_qpdf_passes(spoof_tesseract_cache, poster, outpdf): +def test_userunit_pdf_passes(poster, outpdf): before = PdfInfo(poster) - check_ocrmypdf(poster, outpdf, '--output-type=pdf', env=spoof_tesseract_cache) + check_ocrmypdf( + poster, + outpdf, + '--output-type=pdf', + '--plugin', + 'tests/plugins/tesseract_cache.py', + ) after = PdfInfo(outpdf) assert isclose(before[0].width_inches, after[0].width_inches) -def test_rotate_interaction(spoof_tesseract_cache, poster, outpdf): +def test_rotate_interaction(poster, outpdf): check_ocrmypdf( - poster, outpdf, '--output-type=pdf', '--rotate-pages', env=spoof_tesseract_cache + poster, + outpdf, + '--output-type=pdf', + '--rotate-pages', + '--plugin', + 'tests/plugins/tesseract_cache.py', ) diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 00000000..fd4d6fc2 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,293 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import logging +from unittest.mock import patch + +import pikepdf +import pytest + +from ocrmypdf import _validation as vd +from ocrmypdf._concurrent import NullProgressBar, SerialExecutor +from ocrmypdf._exec.tesseract import TesseractVersion +from ocrmypdf._plugin_manager import get_plugin_manager +from ocrmypdf.api import create_options +from ocrmypdf.cli import get_parser +from ocrmypdf.exceptions import BadArgsError, MissingDependencyError +from ocrmypdf.pdfinfo import PdfInfo + +from .conftest import run_ocrmypdf_api + + +def make_opts_pm(input_file='a.pdf', output_file='b.pdf', language='eng', **kwargs): + if language is not None: + kwargs['language'] = language + parser = get_parser() + pm = get_plugin_manager(kwargs.get('plugins', [])) + pm.hook.add_options(parser=parser) # pylint: disable=no-member + return ( + create_options( + input_file=input_file, output_file=output_file, parser=parser, **kwargs + ), + pm, + ) + + +def make_opts(*args, **kwargs): + opts, _pm = make_opts_pm(*args, **kwargs) + return opts + + +def test_hocr_notlatin_warning(caplog): + # Bypass the test to see if the language is installed; we just want to pretend + # that a non-Latin language is installed + vd._check_options( + *make_opts_pm(language='chi_sim', pdf_renderer='hocr', output_type='pdfa'), + {'chi_sim'}, + ) + assert 'PDF renderer is known to cause' in caplog.text + + +def test_old_ghostscript(caplog): + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.19'): + vd._check_options( + *make_opts_pm(language='chi_sim', output_type='pdfa'), {'chi_sim'} + ) + assert 'does not work correctly' in caplog.text + + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.18'): + with pytest.raises(MissingDependencyError): + vd._check_options(*make_opts_pm(output_type='pdfa-3'), set()) + + with patch('ocrmypdf._exec.ghostscript.version', return_value='9.24'): + with pytest.raises(MissingDependencyError): + vd._check_options(*make_opts_pm(), set()) + + +def test_old_tesseract_error(): + with patch('ocrmypdf._exec.tesseract.version', return_value='4.00.00alpha'): + with pytest.raises(MissingDependencyError): + opts = make_opts(pdf_renderer='sandwich', language='eng') + plugin_manager = get_plugin_manager(opts.plugins) + vd._check_options(opts, plugin_manager, {'eng'}) + + +def test_lossless_redo(): + with pytest.raises(BadArgsError): + vd.check_options_output(make_opts(redo_ocr=True, deskew=True)) + + +def test_mutex_options(): + with pytest.raises(BadArgsError): + vd.check_options_ocr_behavior(make_opts(force_ocr=True, skip_text=True)) + with pytest.raises(BadArgsError): + vd.check_options_ocr_behavior(make_opts(redo_ocr=True, skip_text=True)) + with pytest.raises(BadArgsError): + vd.check_options_ocr_behavior(make_opts(redo_ocr=True, force_ocr=True)) + + +def test_optimizing(caplog): + vd.check_options_optimizing( + make_opts(optimize=0, jbig2_lossy=True, png_quality=18, jpeg_quality=10) + ) + assert 'will be ignored because' in caplog.text + + +def test_user_words(caplog): + with patch('ocrmypdf._exec.tesseract.has_user_words', return_value=False): + opts = make_opts(user_words='foo') + plugin_manager = get_plugin_manager(opts.plugins) + vd._check_options(opts, plugin_manager, set()) + assert '4.0 ignores --user-words' in caplog.text + caplog.clear() + with patch('ocrmypdf._exec.tesseract.has_user_words', return_value=True): + opts = make_opts(user_patterns='foo') + plugin_manager = get_plugin_manager(opts.plugins) + vd._check_options(opts, plugin_manager, set()) + assert '4.0 ignores --user-words' not in caplog.text + + +def test_pillow_options(): + vd.check_options_pillow(make_opts(max_image_mpixels=0)) + + +def test_output_tty(): + with patch('sys.stdout.isatty', return_value=True): + with pytest.raises(BadArgsError): + vd.check_requested_output_file(make_opts(output_file='-')) + + +def test_report_file_size(tmp_path, caplog): + in_ = tmp_path / 'a.pdf' + out = tmp_path / 'b.pdf' + pdf = pikepdf.new() + pdf.save(in_) + pdf.save(out) + opts = make_opts(output_type='pdf') + vd.report_output_file_size(opts, in_, out) + assert caplog.text == '' + caplog.clear() + + waste_of_space = b'Dummy' * 5000 + pdf.Root.Dummy = waste_of_space + pdf.save(in_) + pdf.Root.Dummy2 = waste_of_space + waste_of_space + pdf.save(out) + + with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch( + 'ocrmypdf._validation.pngquant.available', return_value=True + ): + vd.report_output_file_size(opts, in_, out) + assert 'No reason' in caplog.text + caplog.clear() + + with patch('ocrmypdf._validation.jbig2enc.available', return_value=False), patch( + 'ocrmypdf._validation.pngquant.available', return_value=True + ): + vd.report_output_file_size(opts, in_, out) + assert 'optional dependency' in caplog.text + caplog.clear() + + opts = make_opts(in_, out, optimize=0, output_type='pdf') + vd.report_output_file_size(opts, in_, out) + assert 'disabled' in caplog.text + caplog.clear() + + +def test_false_action_store_true(): + opts = make_opts(keep_temporary_files=True) + assert opts.keep_temporary_files + opts = make_opts(keep_temporary_files=False) + assert not opts.keep_temporary_files + + +@pytest.mark.parametrize('progress_bar', [True, False]) +def test_no_progress_bar(progress_bar, resources): + opts = make_opts(progress_bar=progress_bar, input_file=(resources / 'trivial.pdf')) + plugin_manager = get_plugin_manager(opts.plugins) + + vd._check_options(opts, plugin_manager, set()) + + pbar_disabled = None + + class CheckProgressBar(NullProgressBar): + def __init__(self, disable, **kwargs): + nonlocal pbar_disabled + pbar_disabled = disable + super().__init__(disable=disable, **kwargs) + + executor = SerialExecutor(pbar_class=CheckProgressBar) + pdfinfo = PdfInfo(opts.input_file, progbar=opts.progress_bar, executor=executor) + + assert pdfinfo is not None + assert pbar_disabled is not None and pbar_disabled != progress_bar + + +def test_language_warning(caplog): + opts = make_opts(language=None) + _plugin_manager = get_plugin_manager(opts.plugins) + caplog.set_level(logging.DEBUG) + with patch( + 'ocrmypdf._validation.locale.getlocale', return_value=('en_US', 'UTF-8') + ) as mock: + vd.check_options_languages(opts, {'eng'}) + assert opts.languages == {'eng'} + assert '' in caplog.text + mock.assert_called_once() + + opts = make_opts(language=None) + with patch( + 'ocrmypdf._validation.locale.getlocale', return_value=('fr_FR', 'UTF-8') + ) as mock: + vd.check_options_languages(opts, {'eng'}) + assert opts.languages == {'eng'} + assert 'assuming --language' in caplog.text + mock.assert_called_once() + + +def test_version_comparison(): + vd.check_external_program( + program="dummy_basic", + package="dummy", + version_checker=lambda: '9.0', + need_version='8.0.2', + ) + vd.check_external_program( + program="dummy_doubledigit", + package="dummy", + version_checker=lambda: '10.0', + need_version='8.0.2', + ) + with pytest.raises(MissingDependencyError): + vd.check_external_program( + program="tesseract", + package="tesseract", + version_checker=lambda: '4.0.0-beta.1', + need_version='4.0.0', + version_parser=TesseractVersion, + ) + vd.check_external_program( + program="tesseract", + package="tesseract", + version_checker=lambda: 'v5.0.0-alpha.20200201', + need_version='4.0.0', + version_parser=TesseractVersion, + ) + vd.check_external_program( + program="tesseract", + package="tesseract", + version_checker=lambda: '4.1.1-rc2-25-g9707', + need_version='4.0.0', + version_parser=TesseractVersion, + ) + with pytest.raises(MissingDependencyError): + vd.check_external_program( + program="dummy_fails", + package="dummy", + version_checker=lambda: '1.0', + need_version='2.0', + ) + + +def test_optional_program_recommended(caplog): + caplog.clear() + + def raiser(): + raise FileNotFoundError('jbig2') + + with caplog.at_level(logging.WARNING): + vd.check_external_program( + program="jbig2", + package="jbig2enc", + version_checker=raiser, + need_version='42', + required_for='this test case', + recommended=True, + ) + assert any( + (loglevel == logging.WARNING and "recommended" in msg) + for _logger_name, loglevel, msg in caplog.record_tuples + ) + + +def test_pagesegmode_warning(caplog): + opts = make_opts(tesseract_pagesegmode='0') + plugin_manager = get_plugin_manager(opts.plugins) + vd._check_options(opts, plugin_manager, set()) + assert 'disable OCR' in caplog.text + + +def test_two_languages(): + vd._check_options( + *make_opts_pm(language='fakelang1+fakelang2'), {'fakelang1', 'fakelang2'} + ) + + +def test_sidecar_equals_output(resources, no_outpdf): + op = no_outpdf + with pytest.raises(BadArgsError, match=r'--sidecar'): + run_ocrmypdf_api(resources / 'trivial.pdf', op, '--sidecar', op) diff --git a/tests/test_weave.py b/tests/test_weave.py deleted file mode 100644 index 06fa2a0a..00000000 --- a/tests/test_weave.py +++ /dev/null @@ -1,62 +0,0 @@ -# © 2019 James R. Barlow: github.com/jbarlow83 -# -# This file is part of OCRmyPDF. -# -# OCRmyPDF is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# OCRmyPDF is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OCRmyPDF. If not, see . - -import os - -import pytest - -import pikepdf - -check_ocrmypdf = pytest.helpers.check_ocrmypdf - - -def test_no_glyphless_weave(resources, outdir): - pdf = pikepdf.open(resources / 'francais.pdf') - pdf_aspect = pikepdf.open(resources / 'aspect.pdf') - pdf_cmyk = pikepdf.open(resources / 'cmyk.pdf') - pdf.pages.extend(pdf_aspect.pages) - pdf.pages.extend(pdf_cmyk.pages) - pdf.save(outdir / 'test.pdf') - - env = os.environ.copy() - env['_OCRMYPDF_MAX_REPLACE_PAGES'] = '2' - check_ocrmypdf( - outdir / 'test.pdf', - outdir / 'out.pdf', - '--deskew', - '--tesseract-timeout', - '0', - env=env, - ) - - -@pytest.helpers.needs_pdfminer -def test_links(resources, outpdf): - check_ocrmypdf( - resources / 'link.pdf', - outpdf, - '--redo-ocr', - '--oversample', - '200', - '--output-type', - 'pdf', - ) - pdf = pikepdf.open(outpdf) - p1 = pdf.pages[0] - p2 = pdf.pages[1] - assert p1.Annots[0].A.D[0].objgen == p2.objgen - assert p2.Annots[0].A.D[0].objgen == p1.objgen