Compare commits

...
42 Commits
Author SHA1 Message Date
James R. Barlow d303b42c86 Add documentation to PdfMinerState 2024-10-27 22:01:36 -07:00
James R. Barlow f77f701a50 Fix quadratic time performance regression on scanning pages 2024-10-27 21:57:53 -07:00
James R. Barlow 1c3b7d1507 Tidy release notes 2024-10-27 19:35:53 -07:00
James R. Barlow bf62562787 Merge branch 'feature/docker-ubuntu-24' 2024-10-27 17:39:06 -07:00
James R. Barlow 6c6cbfd4d6 Make images extracted for jbig2enc optimization have unique filenames
Fixes #1396
2024-10-27 17:38:52 -07:00
James R. Barlow ee5acbe94e Repair PDF before all processing
Some PDFs choke both pdfminer.six and Ghostscript but the issues can be fixed first.

Fixes #1403
2024-10-27 17:37:46 -07:00
James R. Barlow 5e478a7774 do_get_pdfinfo: typing 2024-10-27 17:23:20 -07:00
James R. Barlow 92c5200ad2 Update release notes for 16.6.0 2024-10-27 16:56:41 -07:00
James R. Barlow 86a102f8e6 Update docker docs 2024-10-27 16:49:54 -07:00
James R. Barlow 2463b91051 Remove unneeded build steps from Alpine 2024-10-27 16:46:57 -07:00
James R. Barlow 07f7c6b812 Reinstate jbig2 building 2024-10-27 16:33:37 -07:00
James R. Barlow 8138664287 Remove Ubuntu 22.04 container 2024-10-27 16:14:08 -07:00
James R. Barlow 120ca72393 Update Ubuntu Dockerfile 2024-10-27 16:12:42 -07:00
James R. Barlow f9b3e9a97b Add Dockerfile for Ubuntu 24.04 LTS 2024-10-27 15:33:34 -07:00
James R. Barlow 1e87930bbb Use uv to construct Alpine Docker image 2024-10-27 15:32:46 -07:00
James R. Barlow fe4725658e unpaper: fix regex 2024-10-27 14:57:21 -07:00
James R. Barlow 9d042767cc Attempt to fix matrix and unpaper version error 2024-10-27 14:04:45 -07:00
James R. Barlow 23bc247b9c Improve Linux coverage matrix 2024-10-27 13:52:09 -07:00
James R. Barlow e44bf46d77 Fix img2pdf and python version 2024-10-27 13:32:06 -07:00
James R. Barlow f50620c244 Convert to uv build 2024-10-27 13:26:41 -07:00
James R. Barlow 6f755321b8 Ignore unpaper warning message when checking version
Fixes #1409
2024-10-27 13:06:30 -07:00
James R. Barlow 706681deb8 Improve some type checks 2024-10-27 12:54:29 -07:00
James R. Barlow c283cf0a0d Fix incorrect return value 2024-10-27 12:32:40 -07:00
James R. Barlow 0f82d7223e Fix some typing issues 2024-10-27 12:31:16 -07:00
James R. Barlow 9a6150ae53 Refactor get_pdfinfo common code 2024-10-27 12:09:33 -07:00
James R. Barlow fec0948a13 If inside a container, remind user that path is relative to container 2024-10-27 11:55:38 -07:00
James R. Barlow 18b59c57b4 Refactor our tests that check if we are in a container 2024-10-27 11:55:22 -07:00
Mayeul KauffmannandGitHub a67a11e61c Doc: new infix for temp files; snap temp files folder (#1404) 2024-10-26 11:18:25 -07:00
James R. Barlow 6ca4940a32 Upgrade docker alpine to latest 3.19.x and add note about 3.20.x 2024-09-15 16:43:35 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e4cce2642 Bump sigstore/gh-action-sigstore-python from 2.1.1 to 3.0.0 (#1392)
Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.1 to 3.0.0.
- [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases)
- [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.1...v3.0.0)

---
updated-dependencies:
- dependency-name: sigstore/gh-action-sigstore-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-05 01:46:26 -07:00
James R. Barlow 8fca0c71dc v16.5.0 release notes 2024-08-31 02:12:07 -07:00
James R. Barlow 944d99bdc1 Fix PROGRAM_NAME 2024-08-31 01:23:55 -07:00
James R. Barlow 5bb6e1c5d7 Modify GitHub release strategy to use sigstore 2024-08-31 01:17:51 -07:00
James R. Barlow 8d7a8f0f98 Update documentation to transition from setuptools to hatchling 2024-08-31 01:14:51 -07:00
James R. Barlow b9dd0a5e3c Use hatchling and hatch-vcs as build backend 2024-08-31 01:08:23 -07:00
James R. Barlow 6949ad2c5d pyproject: link changelog 2024-08-31 00:45:46 -07:00
James R. Barlow b3324c3b4e Don't assume /Mask is always a Stream
Fixes #1377
2024-08-31 00:37:52 -07:00
James R. Barlow b38cac6931 Drop wheel from build requires
Not usually needed anymore
2024-08-28 01:31:56 -07:00
Elliott Sales de AndradeandGitHub bb4c47e707 Fix broken test_rotate_page_level (#1382)
Before 42ff7fc842, `make_rotate_test`
always used `resources / 'typewriter.png'`, but after the change the
second call accidentally used just `resources`, which is a directory,
and fails to open.
2024-08-21 01:25:07 -07:00
James R. Barlow 5e1e2497ab Enable Python 3.13 experimental 2024-08-15 01:32:33 -07:00
James R. Barlow cd910fbf21 Improve "PDF/A conversion failed" message 2024-08-10 01:34:02 -07:00
James R. Barlow 1225269a4b Clarify opporutnities available with OCR_JSON_SETTINGS 2024-08-10 01:02:05 -07:00
38 changed files with 421 additions and 271 deletions
+17 -26
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow # SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
FROM ubuntu:22.04 as base FROM ubuntu:24.04 AS base
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV TZ=UTC ENV TZ=UTC
@@ -9,19 +9,15 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
libqpdf-dev \ python-is-python3
zlib1g \
liblept5
FROM base as builder FROM base AS builder
# Note we need leptonica here to build jbig2 # Note we need leptonica here to build jbig2
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool \ build-essential autoconf automake libtool \
libleptonica-dev \ libleptonica-dev \
zlib1g-dev \ zlib1g-dev \
python3-dev \
python3-distutils \
libffi-dev \ libffi-dev \
ca-certificates \ ca-certificates \
curl \ curl \
@@ -29,15 +25,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libcairo2-dev \ libcairo2-dev \
pkg-config pkg-config
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
RUN \
curl https://bootstrap.pypa.io/get-pip.py | python3
# Compile and install jbig2 # Compile and install jbig2
# Needs libleptonica-dev, zlib1g-dev # Needs libleptonica-dev, zlib1g-dev
RUN \ RUN \
mkdir jbig2 \ mkdir jbig2 \
&& curl -L https://github.com/agl/jbig2enc/archive/ea6a40a.tar.gz | \ && curl -L https://github.com/agl/jbig2enc/archive/c0141bf.tar.gz | \
tar xz -C jbig2 --strip-components=1 \ tar xz -C jbig2 --strip-components=1 \
&& cd jbig2 \ && cd jbig2 \
&& ./autogen.sh && ./configure && make && make install \ && ./autogen.sh && ./configure && make && make install \
@@ -48,23 +40,24 @@ COPY . /app
WORKDIR /app WORKDIR /app
RUN pip3 install --no-cache-dir .[test,webservice,watcher] RUN curl -LsSf https://astral.sh/uv/0.4.27/install.sh | sh
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
# Instead of restarting the shell, use uv directly from its installed location.
RUN /root/.cargo/bin/uv sync --extra test --extra webservice --extra watcher
FROM base FROM base
# For Tesseract 5 RUN apt-get update && apt-get install -y software-properties-common
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common gpg-agent
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
ghostscript \ ghostscript \
fonts-droid-fallback \ fonts-droid-fallback \
jbig2dec \ jbig2dec \
img2pdf \
libsm6 libxext6 libxrender-dev \
pngquant \ pngquant \
python-is-python3 \
tesseract-ocr \ tesseract-ocr \
tesseract-ocr-chi-sim \ tesseract-ocr-chi-sim \
tesseract-ocr-deu \ tesseract-ocr-deu \
@@ -80,11 +73,9 @@ WORKDIR /app
COPY --from=builder /usr/local/lib/ /usr/local/lib/ COPY --from=builder /usr/local/lib/ /usr/local/lib/
COPY --from=builder /usr/local/bin/ /usr/local/bin/ COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /app/misc/webservice.py /app/ COPY --from=builder --chown=app:app /app /app
COPY --from=builder /app/misc/watcher.py /app/
# Copy minimal project files to get the test suite. ENV PATH="/app/.venv/bin:${PATH}"
COPY --from=builder /app/pyproject.toml /app/README.md /app/
COPY --from=builder /app/tests /app/tests ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
+16 -35
View File
@@ -1,7 +1,14 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow # SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
FROM alpine:3.19.1 as base # Note: Alpine 3.20 builds tesseract with --enable-opencl, which is not
# supported by anyone. OCRmyPDF is not compatible with Alpine 3.20.0
# through 3.20.3. The Alpine issue should be fixed in 3.21.0. It is
# not clear if 3.20.4+ will have the fix.
# Details
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/16143
# https://github.com/ocrmypdf/OCRmyPDF/issues/1395
FROM alpine:3.19 AS base
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV TZ=UTC ENV TZ=UTC
@@ -10,40 +17,24 @@ RUN apk add --no-cache \
python3 \ python3 \
zlib zlib
FROM base as builder FROM base AS builder
RUN apk add --no-cache \ RUN apk add --no-cache \
ca-certificates \ ca-certificates \
git \ git \
python3-dev \ python3-dev \
py3-pip curl
# On arm64, we need to build cffi from source.
ARG TARGETPLATFORM
RUN if [ "${TARGETPLATFORM}" == "linux/arm64" ]; then \
apk add --no-cache \
build-base \
autoconf \
automake \
libtool \
zlib-dev \
libffi-dev \
cairo-dev \
pkgconfig \
; \
fi
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN python3 -m venv .venv RUN curl -LsSf https://astral.sh/uv/0.4.27/install.sh | sh
RUN source .venv/bin/activate \ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
&& python3 -m pip install --no-cache-dir --upgrade pip \
&& python3 -m pip install --no-cache-dir wheel \ # Instead of restarting the shell, use uv directly from its installed location.
&& python3 -m pip install --no-cache-dir .[test,webservice,watcher] RUN /root/.cargo/bin/uv sync --extra test --extra webservice --extra watcher
FROM base FROM base
@@ -66,17 +57,7 @@ RUN apk add --no-cache \
WORKDIR /app WORKDIR /app
COPY --from=builder /usr/local/lib/ /usr/local/lib/ COPY --from=builder --chown=app:app /app /app
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /app/.venv/ /app/.venv/
COPY --from=builder /app/misc/webservice.py /app/
COPY --from=builder /app/misc/watcher.py /app/
# Copy minimal project files to get the test suite.
COPY --from=builder /app/pyproject.toml /app/README.md /app/
COPY --from=builder /app/tests /app/tests
ENV PATH="/app/.venv/bin:${PATH}" ENV PATH="/app/.venv/bin:${PATH}"
+67 -49
View File
@@ -21,18 +21,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-22.04, ubuntu-24.04]
python: ["3.10", "3.11", "3.12", "3.13"]
include: include:
- os: ubuntu-22.04 - os: ubuntu-22.04
tesseract_ppa: "ppa"
python: "3.10" python: "3.10"
- os: ubuntu-22.04 - os: ubuntu-24.04
python: "3.11"
- os: ubuntu-22.04
python: "3.10"
tesseract5: true
- os: ubuntu-latest
python: "3.12"
tesseract5: true
- os: ubuntu-latest
python: "pypy3.10" python: "pypy3.10"
env: env:
@@ -44,14 +39,18 @@ jobs:
with: with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v5 - name: Install uv
name: Setup Python uses: astral-sh/setup-uv@v3
with:
version: "0.4.27"
- name: "Set up Python"
uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
cache: "pip"
- name: Install Tesseract 5 - name: Install Tesseract from PPA
if: matrix.tesseract5 if: matrix.tesseract_ppa
run: | run: |
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr-devel sudo add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
@@ -85,8 +84,7 @@ jobs:
- name: Install Python packages - name: Install Python packages
run: | run: |
python -m pip install --upgrade pip wheel uv sync --extra test
python -m pip install --prefer-binary .[test]
- name: Report versions - name: Report versions
run: | run: |
@@ -94,11 +92,11 @@ jobs:
gs --version gs --version
pngquant --version pngquant --version
unpaper --version unpaper --version
img2pdf --version uv run img2pdf --version
- name: Test - name: Test
run: | run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
@@ -115,7 +113,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64 os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
python: ["3.10", "3.11", "3.12"] python: ["3.10", "3.11", "3.12", "3.13"]
env: env:
OS: ${{ matrix.os }} OS: ${{ matrix.os }}
@@ -138,27 +136,30 @@ jobs:
pngquant \ pngquant \
tesseract tesseract
- uses: actions/setup-python@v5 - name: Install uv
name: Setup Python uses: astral-sh/setup-uv@v3
with:
version: "0.4.27"
- name: "Set up Python"
uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
cache: "pip"
- name: Install Python packages - name: Install Python packages
run: | run: |
python -m pip install --upgrade pip wheel uv sync --extra test
python -m pip install --prefer-binary .[test]
- name: Report versions - name: Report versions
run: | run: |
tesseract --version tesseract --version
gs --version gs --version
pngquant --version pngquant --version
img2pdf --version uv run img2pdf --version
- name: Test - name: Test
run: | run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
@@ -174,7 +175,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [windows-latest] os: [windows-latest]
python: ["3.10", "3.11", "3.12"] python: ["3.10", "3.11", "3.12", "3.13"]
env: env:
OS: ${{ matrix.os }} OS: ${{ matrix.os }}
@@ -185,11 +186,15 @@ jobs:
with: with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v5 - name: Install uv
name: Setup Python uses: astral-sh/setup-uv@v3
with:
version: "0.4.27"
- name: "Set up Python"
uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
cache: "pip"
- name: Install system packages - name: Install system packages
run: | run: |
@@ -198,12 +203,11 @@ jobs:
- name: Install Python packages - name: Install Python packages
run: | run: |
python -m pip install --upgrade pip wheel uv sync --extra test
python -m pip install --prefer-binary .[test]
- name: Test - name: Test
run: | run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ uv run pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
@@ -221,16 +225,14 @@ jobs:
with: with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v5 - name: Install uv
name: Setup Python uses: astral-sh/setup-uv@v3
with: with:
python-version: "3.10" version: "0.4.27"
cache: "pip"
- name: Make wheels and sdist - name: Make wheels and sdist
run: | run: |
python -m pip install --upgrade pip wheel build uv build --sdist --wheel
python -m build --sdist --wheel
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
@@ -258,29 +260,45 @@ jobs:
create_release: create_release:
name: Create GitHub release name: Create GitHub release
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows] needs: [upload_pypi]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
permissions: permissions:
# Required to create a release # Required to create a release
contents: write contents: write
id-token: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: artifact name: artifact
path: dist path: dist
- name: Create Release - name: Sign the dists with Sigstore
id: create-release uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: shogo82148/actions-create-release@v1
- name: Upload Assets
uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ steps.create-release.outputs.upload_url }} inputs: >-
asset_path: |
./dist/*.whl
./dist/*.tar.gz ./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
'${{ github.ref_name }}'
--repo '${{ github.repository }}'
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
docker_ubuntu: docker_ubuntu:
name: Build Ubuntu-based Docker image name: Build Ubuntu-based Docker image
+1
View File
@@ -44,3 +44,4 @@ docs/_build/
docs/_static/ docs/_static/
docs/_templates/ docs/_templates/
docs/Makefile docs/Makefile
src/ocrmypdf/_version.py
+6 -2
View File
@@ -454,6 +454,10 @@ whether it succeeded or failed. An example message is:
Temporary working files retained at: Temporary working files retained at:
/tmp/ocrmypdf.io.u20wpz07 /tmp/ocrmypdf.io.u20wpz07
When OCRmyPDF is launched as a snap, this corresponds to the snap filesystem, for instance:
/tmp/snap-private-tmp/snap.ocrmypdf/tmp/ocrmypdf.io.u20wpz07
The organization of this folder is an implementation detail and subject The organization of this folder is an implementation detail and subject
to change between releases. However the general organization is that to change between releases. However the general organization is that
working files on a per page basis have the page number as a prefix working files on a per page basis have the page number as a prefix
@@ -465,9 +469,9 @@ suffix indicates the file type. Some important files include:
on arguments this may differ from the presentation image on arguments this may differ from the presentation image
- ``_pp_deskew.png`` - the image, after deskewing - ``_pp_deskew.png`` - the image, after deskewing
- ``_pp_clean.png`` - the image, after cleaning with unpaper - ``_pp_clean.png`` - the image, after cleaning with unpaper
- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible - ``_ocr_hocr.pdf`` - the OCR file; appears as a blank page with invisible
text embedded text embedded
- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page, - ``_ocr_hocr.txt`` - the OCR text (not necessarily all text on the page,
if the page is mixed format) if the page is mixed format)
- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo - ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo
data structure data structure
+1 -1
View File
@@ -135,7 +135,7 @@ Users may need to customize the script to meet their requirements.
"OCR_ON_SUCCESS_ARCHIVE", "This will move the processed original file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted." "OCR_ON_SUCCESS_ARCHIVE", "This will move the processed original file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted."
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``" "OCR_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_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_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true, ""optimize"": "3"}'``."
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval" "OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
"OCR_LOGLEVEL", "Level of log messages to report" "OCR_LOGLEVEL", "Level of log messages to report"
+7 -1
View File
@@ -65,7 +65,13 @@ The ``ocrmypdf`` image is also available, but is deprecated and will be removed
in the future. in the future.
OCRmyPDF will use all available CPU cores. See the Docker documentation for OCRmyPDF will use all available CPU cores. See the Docker documentation for
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__. `adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__
if you are using Docker on macOS or Windows, where you may need to manually assign
more resources. On Linux, all resources will be available automatically.
The underlying operating system and other details in Docker images are subject
to change at minor releases. If you are modifying the image, you should pin
the version you intend to use.
Using the Docker image on the command line Using the Docker image on the command line
========================================== ==========================================
+1 -2
View File
@@ -640,8 +640,7 @@ environment:
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
Or, to install in `development Or, to install in editable mode
mode <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode>`__,
allowing customization of OCRmyPDF, use the ``-e`` flag: allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash .. code-block:: bash
+3 -2
View File
@@ -38,12 +38,13 @@ on ARM and x86_64. Performance may be poor on other processor architectures.
Versioning scheme Versioning scheme
----------------- -----------------
OCRmyPDF uses setuptools-scm for versioning, which derives the version from OCRmyPDF uses hatch-vcs for versioning, which derives the version from
Git as a single source of truth. This may be unsuitable for some distributions, e.g. Git as a single source of truth. This may be unsuitable for some distributions, e.g.
to indicate that your distribution modifies OCRmyPDF in some way. to indicate that your distribution modifies OCRmyPDF in some way.
You can patch the ``__version__`` variable in ``src/ocrmypdf/_version.py`` if You can patch the ``__version__`` variable in ``src/ocrmypdf/_version.py`` if
necessary. necessary, or set the environment variable ``SETUPTOOLS_SCM_PRETEND_VERSION``
to the required version, if you need to override versioning for some reason.
jbig2enc jbig2enc
-------- --------
+9 -12
View File
@@ -29,6 +29,9 @@ conventions. Note that: plugins installed with as setuptools entrypoints are
not checked currently, because OCRmyPDF assumes you may not want to enable not checked currently, because OCRmyPDF assumes you may not want to enable
plugins for all files. plugins for all files.
See [OCRmyPDF-EasyOCR](https://github.com/ocrmypdf/OCRmyPDF-EasyOCR) for an
example of a straightforward, fully working plugin.
Script plugins Script plugins
============== ==============
@@ -70,14 +73,15 @@ similar to ``pytest`` packages such as ``pytest-cov`` (the package) and
module), just like pytest plugins. At the same time, please make it clear module), just like pytest plugins. At the same time, please make it clear
that your package is not official. that your package is not official.
Setuptools plugins Plugins
================== =======
You can also create a plugin that OCRmyPDF will always automatically load if both are 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. installed in the same virtual environment, using a project entrypoint.
OCRmyPDF uses the entrypoint namespace "ocrmypdf".
Your package's ``pyproject.toml`` would need to contain the following, for a plugin For example, ``pyproject.toml`` would need to contain the following, for a plugin named
named ``ocrmypdf-exampleplugin``: ``ocrmypdf-exampleplugin``:
.. code-block:: toml .. code-block:: toml
@@ -87,13 +91,6 @@ named ``ocrmypdf-exampleplugin``:
[project.entry-points."ocrmypdf"] [project.entry-points."ocrmypdf"]
exampleplugin = "exampleplugin.pluginmodule" exampleplugin = "exampleplugin.pluginmodule"
.. code-block:: ini
# equivalent setup.cfg
[options.entry_points]
ocrmypdf =
exampleplugin = exampleplugin.pluginmodule
Plugin requirements Plugin requirements
=================== ===================
+27
View File
@@ -30,6 +30,33 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg .. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.6.0
=======
- Fixed an issue where damaged PDFs would fail with ``--redo-ocr``. :issue:`1403`
- Fixed an error that prevented JBIG2 optimization on Windows if the image
was optimized in an earlier step. :issue:`1396`
- Fixed an error detecting the version of unpaper 7.0.0. :issue:`1409`
- Fixed a performance regression when scanning pages. :issue:`1378`. Thanks @aliemjay.
- Fixed Alpine Docker image by enforcing Alpine 3.19. Alpine 3.20 includes a
defective version of Tesseract OCR and so is not usable.
- Upgraded Ubuntu Docker image to use Ubuntu 24.04.
- Build and test scripts/actions switched to uv.
- When running in a container, we now remind the user that temporary folders
are inside the container and may not be accessible.
- Fixed Linux test coverage matrix, which was missing some key versions.
v16.5.0
=======
- Fixed issue with interpreting PDFs that have images with array masks.
:issue:`1377`
- Enabled testing on Python 3.13.
- Fixed a test that did not work correctly but still passed. :issue:`1382`
- Improved "PDF/A conversion failed" warning message to better describe implications.
- Updated documentation to better explain OCR_JSON_SETTINGS in batch processing.
- Build backend changed from setuptools to hatchling.
v16.4.3 v16.4.3
======= =======
+7 -9
View File
@@ -1,8 +1,8 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow # SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
[build-system] [build-system]
requires = ["setuptools >= 61", "setuptools_scm[toml] >= 7.0.5", "wheel"] requires = ["hatchling", "hatch-vcs"]
build-backend = "setuptools.build_meta" build-backend = "hatchling.build"
[project] [project]
name = "ocrmypdf" name = "ocrmypdf"
@@ -46,6 +46,7 @@ keywords = ["PDF", "OCR", "optical character recognition", "PDF/A", "scanning"]
Documentation = "https://ocrmypdf.readthedocs.io/" Documentation = "https://ocrmypdf.readthedocs.io/"
Source = "https://github.com/ocrmypdf/OCRmyPDF" Source = "https://github.com/ocrmypdf/OCRmyPDF"
Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues" Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
Changelog = "https://github.com/ocrmypdf/OCRmyPDF/docs/release_notes.rst"
[project.optional-dependencies] [project.optional-dependencies]
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"] docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
@@ -67,14 +68,11 @@ webservice = ["Flask>=2.0.1"]
[project.scripts] [project.scripts]
ocrmypdf = "ocrmypdf.__main__:run" ocrmypdf = "ocrmypdf.__main__:run"
[tool.setuptools.package-data] [tool.hatch.version]
ocrmypdf = ["data/sRGB.icc", "py.typed"] source = "vcs"
[tool.setuptools.packages.find] [tool.hatch.build.hooks.vcs]
where = ["src"] version-file = "src/ocrmypdf/_version.py"
namespaces = false
[tool.setuptools_scm]
[tool.distutils.bdist_wheel] [tool.distutils.bdist_wheel]
python-tag = "py310" python-tag = "py310"
+2 -2
View File
@@ -9,11 +9,12 @@ from pluggy import HookimplMarker as _HookimplMarker
from ocrmypdf import helpers, hocrtransform, pdfa, pdfinfo from ocrmypdf import helpers, hocrtransform, pdfa, pdfinfo
from ocrmypdf._concurrent import Executor from ocrmypdf._concurrent import Executor
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipelines._common import ( from ocrmypdf._pipelines._common import (
configure_debug_logging, configure_debug_logging,
) )
from ocrmypdf._version import PROGRAM_NAME, __version__ from ocrmypdf._version import __version__
from ocrmypdf.api import ( from ocrmypdf.api import (
Verbosity, Verbosity,
configure_logging, configure_logging,
@@ -37,7 +38,6 @@ from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence
hookimpl = _HookimplMarker('ocrmypdf') hookimpl = _HookimplMarker('ocrmypdf')
__all__ = [ __all__ = [
'__version__', '__version__',
'BadArgsError', 'BadArgsError',
+2
View File
@@ -6,3 +6,5 @@ DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold # Default rotation threshold
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0 DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
PROGRAM_NAME = 'OCRmyPDF'
+1 -1
View File
@@ -48,7 +48,7 @@ class UnpaperImageTooLargeError(Exception):
def version() -> Version: def version() -> Version:
return Version(get_version('unpaper')) return Version(get_version('unpaper', regex=r'(?m).*?(\d+(\.\d+)(\.\d+)?)'))
@contextmanager @contextmanager
+1 -1
View File
@@ -15,8 +15,8 @@ from pikepdf import Dictionary, Name, Pdf
from pikepdf import __version__ as PIKEPDF_VERSION from pikepdf import __version__ as PIKEPDF_VERSION
from pikepdf.models.metadata import PdfMetadata, encode_pdf_date from pikepdf.models.metadata import PdfMetadata, encode_pdf_date
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PdfContext from ocrmypdf._jobcontext import PdfContext
from ocrmypdf._version import PROGRAM_NAME
from ocrmypdf._version import __version__ as OCRMYPF_VERSION from ocrmypdf._version import __version__ as OCRMYPF_VERSION
from ocrmypdf.languages import iso_639_2_from_3 from ocrmypdf.languages import iso_639_2_from_3
+8 -3
View File
@@ -159,8 +159,13 @@ def triage(
"Argument --image-dpi is being ignored because the " "Argument --image-dpi is being ignored because the "
"input file is a PDF, not an image." "input file is a PDF, not an image."
) )
# Origin file is a pdf create a symlink with pdf extension try:
safe_symlink(input_file, output_file) with pikepdf.open(input_file) as pdf:
pdf.save(output_file)
except pikepdf.PdfError as e:
raise InputFileError() from e
except pikepdf.PasswordError as e:
raise EncryptedPdfError() from e
return output_file return output_file
except OSError as e: except OSError as e:
log.debug(f"Temporary file was at: {input_file}") log.debug(f"Temporary file was at: {input_file}")
@@ -854,7 +859,7 @@ def fix_pagepdf_boxes(
page.CropBox = cropbox page.CropBox = cropbox
page.TrimBox = trimbox page.TrimBox = trimbox
pdf.save(out_file) pdf.save(out_file)
return pdf return out_file
def generate_postscript_stub(context: PdfContext) -> Path: def generate_postscript_stub(context: PdfContext) -> Path:
+38 -6
View File
@@ -33,6 +33,7 @@ from ocrmypdf._pipeline import (
generate_postscript_stub, generate_postscript_stub,
get_orientation_correction, get_orientation_correction,
get_pdf_save_settings, get_pdf_save_settings,
get_pdfinfo,
optimize_pdf, optimize_pdf,
preprocess_clean, preprocess_clean,
preprocess_deskew, preprocess_deskew,
@@ -51,9 +52,12 @@ from ocrmypdf.helpers import (
available_cpu_count, available_cpu_count,
check_pdf, check_pdf,
pikepdf_enable_mmap, pikepdf_enable_mmap,
running_in_docker,
running_in_snap,
samefile, samefile,
) )
from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import PdfInfo
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
tls = threading.local() tls = threading.local()
@@ -183,7 +187,7 @@ def configure_debug_logging(
return log_file_handler, remover return log_file_handler, remover
def worker_init(max_pixels: int) -> None: def worker_init(max_pixels: int | None) -> None:
"""Initialize a worker thread or process.""" """Initialize a worker thread or process."""
# In Windows, child process will not inherit our change to this value in # In Windows, child process will not inherit our change to this value in
# the parent process, so ensure workers get it set. Not needed when running # the parent process, so ensure workers get it set. Not needed when running
@@ -215,6 +219,22 @@ def manage_debug_log_handler(
remover() remover()
def _print_temp_folder_location(work_folder: Path):
"""Print the location of the temporary work folder."""
msgs = [f"Temporary working files retained at:\n{work_folder}"]
if running_in_docker(): # pragma: no cover
msgs.append(
"OCRmyPDF is running in a Docker container, "
"so the files will be inside the container."
)
elif running_in_snap(): # pragma: no cover
msgs.append(
"OCRmyPDF is running in a Snap container, "
"so the files will be inside the container."
)
print('\n'.join(msgs), file=sys.stderr)
@contextmanager @contextmanager
def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool): def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool):
try: try:
@@ -222,10 +242,7 @@ def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool)
finally: finally:
if retain: if retain:
if print_location: if print_location:
print( _print_temp_folder_location(work_folder)
f"Temporary working files retained at:\n{work_folder}",
file=sys.stderr,
)
else: else:
shutil.rmtree(work_folder, ignore_errors=True) shutil.rmtree(work_folder, ignore_errors=True)
@@ -300,6 +317,20 @@ def setup_pipeline(
return executor return executor
def do_get_pdfinfo(
pdf_path: Path, executor: Executor, options: argparse.Namespace
) -> PdfInfo:
return get_pdfinfo(
pdf_path,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs,
use_threads=options.use_threads,
check_pages=options.pages,
)
def preprocess( def preprocess(
page_context: PageContext, page_context: PageContext,
image: Path, image: Path,
@@ -441,7 +472,8 @@ def report_output_pdf(options, start_input_file, optimize_messages) -> ExitCode:
log.info("Output file is a %s (as expected)", pdfa_info['conformance']) log.info("Output file is a %s (as expected)", pdfa_info['conformance'])
else: else:
log.warning( log.warning(
"Output file is okay but is not PDF/A (seems to be %s)", "Output file is a valid PDF, but conversion to PDF/A did not "
"succeed (issue: %s)",
pdfa_info['conformance'], pdfa_info['conformance'],
) )
return ExitCode.pdfa_conversion_failed return ExitCode.pdfa_conversion_failed
+2 -10
View File
@@ -19,11 +19,11 @@ from ocrmypdf._graft import OcrGrafter
from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import ( from ocrmypdf._pipeline import (
copy_final, copy_final,
get_pdfinfo,
render_hocr_page, render_hocr_page,
) )
from ocrmypdf._pipelines._common import ( from ocrmypdf._pipelines._common import (
HOCRResult, HOCRResult,
do_get_pdfinfo,
manage_work_folder, manage_work_folder,
postprocess, postprocess,
report_output_pdf, report_output_pdf,
@@ -117,15 +117,7 @@ def run_hocr_to_ocr_pdf_pipeline(
origin_pdf = work_folder / 'origin.pdf' origin_pdf = work_folder / 'origin.pdf'
# Gather pdfinfo and create context # Gather pdfinfo and create context
pdfinfo = get_pdfinfo( pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
origin_pdf,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs,
use_threads=options.use_threads,
check_pages=options.pages,
)
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager) context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
plugin_manager.hook.check_options(options=options) plugin_manager.hook.check_options(options=options)
optimize_messages = exec_hocr_to_ocr_pdf(context, executor) optimize_messages = exec_hocr_to_ocr_pdf(context, executor)
+2 -11
View File
@@ -21,7 +21,6 @@ from ocrmypdf._graft import OcrGrafter
from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import ( from ocrmypdf._pipeline import (
copy_final, copy_final,
get_pdfinfo,
is_ocr_required, is_ocr_required,
merge_sidecars, merge_sidecars,
ocr_engine_hocr, ocr_engine_hocr,
@@ -33,6 +32,7 @@ from ocrmypdf._pipeline import (
from ocrmypdf._pipelines._common import ( from ocrmypdf._pipelines._common import (
PageResult, PageResult,
cli_exception_handler, cli_exception_handler,
do_get_pdfinfo,
manage_debug_log_handler, manage_debug_log_handler,
manage_work_folder, manage_work_folder,
postprocess, postprocess,
@@ -171,16 +171,7 @@ def _run_pipeline(
) )
# Gather pdfinfo and create context # Gather pdfinfo and create context
pdfinfo = get_pdfinfo( pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
origin_pdf,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs,
use_threads=options.use_threads,
check_pages=options.pages,
)
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager) context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
# Validate options are okay for this pdf # Validate options are okay for this pdf
+4 -11
View File
@@ -17,13 +17,13 @@ import PIL
from ocrmypdf._concurrent import Executor from ocrmypdf._concurrent import Executor
from ocrmypdf._jobcontext import PageContext, PdfContext from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import ( from ocrmypdf._pipeline import (
get_pdfinfo,
is_ocr_required, is_ocr_required,
ocr_engine_hocr, ocr_engine_hocr,
validate_pdfinfo_options, validate_pdfinfo_options,
) )
from ocrmypdf._pipelines._common import ( from ocrmypdf._pipelines._common import (
HOCRResult, HOCRResult,
do_get_pdfinfo,
manage_work_folder, manage_work_folder,
process_page, process_page,
set_thread_pageno, set_thread_pageno,
@@ -94,18 +94,11 @@ def run_hocr_pipeline(
work_folder=options.output_folder, retain=True, print_location=False work_folder=options.output_folder, retain=True, print_location=False
) as work_folder: ) as work_folder:
executor = setup_pipeline(options, plugin_manager) executor = setup_pipeline(options, plugin_manager)
shutil.copy2(options.input_file, work_folder / 'origin.pdf') origin_pdf = work_folder / 'origin.pdf'
shutil.copy2(options.input_file, origin_pdf)
# Gather pdfinfo and create context # Gather pdfinfo and create context
pdfinfo = get_pdfinfo( pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
options.input_file,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs,
use_threads=options.use_threads,
check_pages=options.pages,
)
context = PdfContext( context = PdfContext(
options, work_folder, options.input_file, pdfinfo, plugin_manager options, work_folder, options.input_file, pdfinfo, plugin_manager
) )
+9 -15
View File
@@ -28,7 +28,13 @@ from ocrmypdf.exceptions import (
MissingDependencyError, MissingDependencyError,
OutputFileAccessError, OutputFileAccessError,
) )
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink from ocrmypdf.helpers import (
is_file_writable,
monotonic,
running_in_docker,
running_in_snap,
safe_symlink,
)
from ocrmypdf.subprocess import check_external_program from ocrmypdf.subprocess import check_external_program
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -237,18 +243,6 @@ def check_options(options: Namespace, plugin_manager: PluginManager) -> None:
_check_plugin_options(options, plugin_manager) _check_plugin_options(options, plugin_manager)
def _in_docker():
return Path('/.dockerenv').exists()
def _in_snap():
try:
cgroup_text = Path('/proc/self/cgroup').read_text()
return 'snap.ocrmypdf' in cgroup_text
except FileNotFoundError:
return False
def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]: def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]:
if options.input_file == '-': if options.input_file == '-':
# stdin # stdin
@@ -272,7 +266,7 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
return target, os.fspath(options.input_file) return target, os.fspath(options.input_file)
except FileNotFoundError as e: except FileNotFoundError as e:
msg = f"File not found - {options.input_file}" msg = f"File not found - {options.input_file}"
if _in_docker(): # pragma: no cover if running_in_docker(): # pragma: no cover
msg += ( msg += (
"\nDocker cannot access your working directory unless you " "\nDocker cannot access your working directory unless you "
"explicitly share it with the Docker container and set up" "explicitly share it with the Docker container and set up"
@@ -282,7 +276,7 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf" "\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf"
"\n" "\n"
) )
elif _in_snap(): # pragma: no cover elif running_in_snap(): # pragma: no cover
msg += ( msg += (
"\nSnap applications cannot access files outside of " "\nSnap applications cannot access files outside of "
"your home directory unless you explicitly allow it. " "your home directory unless you explicitly allow it. "
-16
View File
@@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Get version by introspecting package information.
OCRmyPDF uses setuptools_scm to derive version from git tags.
"""
from __future__ import annotations
from importlib.metadata import version as _package_version
PROGRAM_NAME = 'ocrmypdf'
# Official PEP 396
__version__ = _package_version('ocrmypdf')
+7 -7
View File
@@ -14,7 +14,7 @@ from collections.abc import Iterable, Sequence
from enum import IntEnum from enum import IntEnum
from io import IOBase from io import IOBase
from pathlib import Path from pathlib import Path
from typing import AnyStr, BinaryIO from typing import BinaryIO
from warnings import warn from warnings import warn
import pluggy import pluggy
@@ -28,7 +28,7 @@ from ocrmypdf._validation import check_options
from ocrmypdf.cli import ArgumentParser, get_parser from ocrmypdf.cli import ArgumentParser, get_parser
from ocrmypdf.helpers import is_iterable_notstr from ocrmypdf.helpers import is_iterable_notstr
StrPath = Path | AnyStr StrPath = Path | str | bytes
PathOrIO = BinaryIO | StrPath PathOrIO = BinaryIO | StrPath
# Installing plugins affects the global state of the Python interpreter, # Installing plugins affects the global state of the Python interpreter,
@@ -140,9 +140,9 @@ def configure_logging(
def _kwargs_to_cmdline( def _kwargs_to_cmdline(
*, defer_kwargs: set[str], **kwargs *, defer_kwargs: set[str], **kwargs
) -> tuple[list[str], dict[str, AnyStr]]: ) -> tuple[list[str | bytes], dict[str, str | bytes]]:
"""Convert kwargs to command line arguments.""" """Convert kwargs to command line arguments."""
cmdline = [] cmdline: list[str | bytes] = []
deferred = {} deferred = {}
for arg, val in kwargs.items(): for arg, val in kwargs.items():
if val is None: if val is None:
@@ -279,7 +279,7 @@ def ocr( # noqa: D417
fast_web_view: float | None = None, fast_web_view: float | None = None,
continue_on_soft_render_error: bool | None = None, continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None, invalidate_digital_signatures: bool | None = None,
plugins: Iterable[StrPath] | None = None, plugins: Iterable[Path | str] | None = None,
plugin_manager=None, plugin_manager=None,
keep_temporary_files: bool | None = None, keep_temporary_files: bool | None = None,
progress_bar: bool | None = None, progress_bar: bool | None = None,
@@ -420,7 +420,7 @@ def _pdf_to_hocr( # noqa: D417
continue_on_soft_render_error: bool | None = None, continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None, invalidate_digital_signatures: bool | None = None,
plugin_manager=None, plugin_manager=None,
plugins: Sequence[StrPath] | None = None, plugins: Sequence[Path | str] | None = None,
keep_temporary_files: bool | None = None, keep_temporary_files: bool | None = None,
**kwargs, **kwargs,
): ):
@@ -491,7 +491,7 @@ def _hocr_to_ocr_pdf( # noqa: D417
color_conversion_strategy: str | None = None, color_conversion_strategy: str | None = None,
fast_web_view: float | None = None, fast_web_view: float | None = None,
plugin_manager=None, plugin_manager=None,
plugins: Sequence[StrPath] | None = None, plugins: Sequence[Path | str] | None = None,
**kwargs, **kwargs,
): ):
"""Run OCRmyPDF on a work folder and produce an output PDF. """Run OCRmyPDF on a work folder and produce an output PDF.
+1 -1
View File
@@ -10,7 +10,7 @@ from collections.abc import Callable, Mapping
from typing import Any, TypeVar from typing import Any, TypeVar
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION from ocrmypdf._version import __version__ as _VERSION
T = TypeVar('T', int, float) T = TypeVar('T', int, float)
+14
View File
@@ -335,3 +335,17 @@ def pikepdf_enable_mmap() -> None:
) )
except AttributeError: except AttributeError:
log.debug("pikepdf mmap not available") log.debug("pikepdf mmap not available")
def running_in_docker() -> bool:
"""Returns True if we seem to be running in a Docker container."""
return Path('/.dockerenv').exists()
def running_in_snap() -> bool:
"""Returns True if we seem to be running in a Snap container."""
try:
cgroup_text = Path('/proc/self/cgroup').read_text()
return 'snap.ocrmypdf' in cgroup_text
except FileNotFoundError:
return False
+5 -5
View File
@@ -357,7 +357,7 @@ class HocrTransform:
line_matrix: Matrix, line_matrix: Matrix,
text: Text, text: Text,
fontsize: float, fontsize: float,
elem: Element, elem: Element | None,
next_elem: Element | None, next_elem: Element | None,
text_direction: TextDirection, text_direction: TextDirection,
inject_word_breaks: bool, inject_word_breaks: bool,
@@ -434,7 +434,7 @@ class HocrTransform:
if ocr_par is None: if ocr_par is None:
continue continue
canvas.do.rect( canvas.do.rect(
ocr_par.llx, ocr_par.lly, ocr_par.width, ocr_par.height, fill=0 ocr_par.llx, ocr_par.lly, ocr_par.width, ocr_par.height, fill=False
) )
def _debug_draw_line_bbox(self, canvas: Canvas, line_box: Rectangle, color=BLUE): def _debug_draw_line_bbox(self, canvas: Canvas, line_box: Rectangle, color=BLUE):
@@ -443,7 +443,7 @@ class HocrTransform:
return return
with canvas.do.save_state(): with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(0.15).rect( canvas.do.stroke_color(color).line_width(0.15).rect(
line_box.llx, line_box.lly, line_box.width, line_box.height, fill=0 line_box.llx, line_box.lly, line_box.width, line_box.height, fill=False
) )
def _debug_draw_word_triangle( def _debug_draw_word_triangle(
@@ -467,7 +467,7 @@ class HocrTransform:
return return
with canvas.do.save_state(): with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(line_width).rect( canvas.do.stroke_color(color).line_width(line_width).rect(
box.llx, box.lly, box.width, box.height, fill=0 box.llx, box.lly, box.width, box.height, fill=False
) )
def _debug_draw_space_bbox( def _debug_draw_space_bbox(
@@ -478,7 +478,7 @@ class HocrTransform:
return return
with canvas.do.save_state(): with canvas.do.save_state():
canvas.do.fill_color(color).line_width(line_width).rect( canvas.do.fill_color(color).line_width(line_width).rect(
box.llx, box.lly, box.width, box.height, fill=1 box.llx, box.lly, box.width, box.height, fill=True
) )
def _debug_draw_baseline( def _debug_draw_baseline(
+12 -4
View File
@@ -75,10 +75,15 @@ def extract_image_filter(
"""Determine if an image is extractable.""" """Determine if an image is extractable."""
if image.Subtype != Name.Image: if image.Subtype != Name.Image:
return None return None
if image.Length < 100: if not isinstance(image.Length, int) or image.Length < 100:
log.debug(f"xref {xref}: skipping image with small stream size") log.debug(f"xref {xref}: skipping image with small stream size")
return None return None
if image.Width < 8 or image.Height < 8: # Issue 732 if (
not isinstance(image.Width, int)
or not isinstance(image.Height, int)
or image.Width < 8
or image.Height < 8
): # Issue 732
log.debug(f"xref {xref}: skipping image with unusually small dimensions") log.debug(f"xref {xref}: skipping image with unusually small dimensions")
return None return None
@@ -154,7 +159,10 @@ def extract_image_jbig2(
imgname = root / f'{xref:08d}' imgname = root / f'{xref:08d}'
with imgname.open('wb') as f: with imgname.open('wb') as f:
ext = pim.extract_to(stream=f) ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext)) # Rename the file so it has .prejbig2.ext extension
# Making it unique avoids problems with Windows if the
# same image is extracted multiple times
imgname.rename(imgname.with_suffix(".prejbig2" + ext))
except NotImplementedError as e: except NotImplementedError as e:
if '/Decode' in str(e): if '/Decode' in str(e):
log.debug( log.debug(
@@ -170,7 +178,7 @@ def extract_image_jbig2(
pim.obj.ColorSpace = colorspace pim.obj.ColorSpace = colorspace
else: else:
del pim.obj.ColorSpace del pim.obj.ColorSpace
return XrefExt(xref, ext) return XrefExt(xref, ".prejbig2" + ext)
return None return None
+55 -24
View File
@@ -12,7 +12,7 @@ import re
import statistics import statistics
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager from contextlib import contextmanager, nullcontext
from decimal import Decimal from decimal import Decimal
from enum import Enum, auto from enum import Enum, auto
from functools import partial from functools import partial
@@ -24,6 +24,7 @@ from warnings import warn
from pdfminer.layout import LTPage, LTTextBox from pdfminer.layout import LTPage, LTTextBox
from pikepdf import ( from pikepdf import (
Dictionary,
Matrix, Matrix,
Name, Name,
Object, Object,
@@ -40,7 +41,12 @@ from ocrmypdf._concurrent import Executor, SerialExecutor
from ocrmypdf._progressbar import ProgressBar from ocrmypdf._progressbar import ProgressBar
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap
from ocrmypdf.pdfinfo.layout import LTStateAwareChar, get_page_analysis, get_text_boxes from ocrmypdf.pdfinfo.layout import (
LTStateAwareChar,
PdfMinerState,
get_page_analysis,
get_text_boxes,
)
logger = logging.getLogger() logger = logging.getLogger()
@@ -378,12 +384,17 @@ class ImageInfo:
# itself. Some PDF writers use this to create a grayscale stencil # itself. Some PDF writers use this to create a grayscale stencil
# mask. For our purposes, the effective size is the size of the # mask. For our purposes, the effective size is the size of the
# larger component (image or smask). # larger component (image or smask).
self._width = max(smask.get(Name.Width, 0), self._width) if isinstance(smask, Stream | Dictionary):
self._height = max(smask.get(Name.Height, 0), self._height) self._width = max(smask.get(Name.Width, 0), self._width)
self._height = max(smask.get(Name.Height, 0), self._height)
if (mask := pim.obj.get(Name.Mask, None)) is not None: if (mask := pim.obj.get(Name.Mask, None)) is not None:
# If the image has a /Mask entry, it has an explicit mask. # If the image has a /Mask entry, it has an explicit mask.
self._width = max(mask.get(Name.Width, 0), self._width) # /Mask can be a Stream or an Array. If it's a Stream,
self._height = max(mask.get(Name.Height, 0), self._height) # use its /Width and /Height if they are larger than the main
# image's.
if isinstance(mask, Stream | Dictionary):
self._width = max(mask.get(Name.Width, 0), self._width)
self._height = max(mask.get(Name.Height, 0), self._height)
# If /ImageMask is true, then this image is a stencil mask # If /ImageMask is true, then this image is a stencil mask
# (Images that draw with this stencil mask will have a reference to # (Images that draw with this stencil mask will have a reference to
@@ -696,13 +707,13 @@ def _page_has_text(text_blocks: Iterable[FloatRect], page_width, page_height) ->
def simplify_textboxes( def simplify_textboxes(
miner: LTPage, textbox_getter: Callable[[LTPage], Iterator[LTTextBox]] miner_page: LTPage, textbox_getter: Callable[[LTPage], Iterator[LTTextBox]]
) -> Iterator[TextboxInfo]: ) -> Iterator[TextboxInfo]:
"""Extract only limited content from text boxes. """Extract only limited content from text boxes.
We do this to save memory and ensure that our objects are pickleable. We do this to save memory and ensure that our objects are pickleable.
""" """
for box in textbox_getter(miner): for box in textbox_getter(miner_page):
first_line = box._objs[0] # pylint: disable=protected-access first_line = box._objs[0] # pylint: disable=protected-access
first_char = first_line._objs[0] # pylint: disable=protected-access first_char = first_line._objs[0] # pylint: disable=protected-access
if not isinstance(first_char, LTStateAwareChar): if not isinstance(first_char, LTStateAwareChar):
@@ -749,9 +760,12 @@ def _pdf_pageinfo_sync(
infile: Path, infile: Path,
check_pages: Container[int], check_pages: Container[int],
detailed_analysis: bool, detailed_analysis: bool,
miner_state: PdfMinerState | None,
) -> PageInfo: ) -> PageInfo:
with _pdf_pageinfo_sync_pdf(thread_pdf, infile) as pdf: with _pdf_pageinfo_sync_pdf(thread_pdf, infile) as pdf:
return PageInfo(pdf, pageno, infile, check_pages, detailed_analysis) return PageInfo(
pdf, pageno, infile, check_pages, detailed_analysis, miner_state
)
def _pdf_pageinfo_concurrent( def _pdf_pageinfo_concurrent(
@@ -763,6 +777,7 @@ def _pdf_pageinfo_concurrent(
progbar, progbar,
check_pages, check_pages,
detailed_analysis: bool = False, detailed_analysis: bool = False,
miner_state: PdfMinerState | None = None,
) -> Sequence[PageInfo | None]: ) -> Sequence[PageInfo | None]:
pages: list[PageInfo | None] = [None] * len(pdf.pages) pages: list[PageInfo | None] = [None] * len(pdf.pages)
@@ -794,7 +809,8 @@ def _pdf_pageinfo_concurrent(
initial_pdf = pdf if use_threads else None initial_pdf = pdf if use_threads else None
contexts = ( contexts = (
(n, initial_pdf, infile, check_pages, detailed_analysis) for n in range(total) (n, initial_pdf, infile, check_pages, detailed_analysis, miner_state)
for n in range(total)
) )
assert n_workers == 1 if use_threads else n_workers >= 1, "Not multithreadable" assert n_workers == 1 if use_threads else n_workers >= 1, "Not multithreadable"
logger.debug( logger.debug(
@@ -861,12 +877,15 @@ class PageInfo:
infile: PathLike, infile: PathLike,
check_pages: Container[int], check_pages: Container[int],
detailed_analysis: bool = False, detailed_analysis: bool = False,
miner_state: PdfMinerState | None = None,
): ):
"""Initialize a PageInfo object.""" """Initialize a PageInfo object."""
self._pageno = pageno self._pageno = pageno
self._infile = infile self._infile = infile
self._detailed_analysis = detailed_analysis self._detailed_analysis = detailed_analysis
self._gather_pageinfo(pdf, pageno, infile, check_pages, detailed_analysis) self._gather_pageinfo(
pdf, pageno, infile, check_pages, detailed_analysis, miner_state
)
def _gather_pageinfo( def _gather_pageinfo(
self, self,
@@ -875,6 +894,7 @@ class PageInfo:
infile: PathLike, infile: PathLike,
check_pages: Container[int], check_pages: Container[int],
detailed_analysis: bool, detailed_analysis: bool,
miner_state: PdfMinerState | None,
): ):
page: Page = pdf.pages[pageno] page: Page = pdf.pages[pageno]
mediabox = [Decimal(d) for d in page.mediabox.as_list()] mediabox = [Decimal(d) for d in page.mediabox.as_list()]
@@ -890,10 +910,11 @@ class PageInfo:
check_this_page = pageno in check_pages check_this_page = pageno in check_pages
if check_this_page and detailed_analysis: if check_this_page and detailed_analysis:
pscript5_mode = str(pdf.docinfo.get(Name.Creator)).startswith('PScript5') page_analysis = miner_state.get_page_analysis(pageno)
miner = get_page_analysis(infile, pageno, pscript5_mode) if page_analysis is not None:
if miner is not None: self._textboxes = list(
self._textboxes = list(simplify_textboxes(miner, get_text_boxes)) simplify_textboxes(page_analysis, get_text_boxes)
)
else: else:
self._textboxes = [] self._textboxes = []
bboxes = (box.bbox for box in self._textboxes) bboxes = (box.bbox for box in self._textboxes)
@@ -1146,16 +1167,26 @@ class PdfInfo:
with Pdf.open(infile) as pdf: with Pdf.open(infile) as pdf:
if pdf.is_encrypted: if pdf.is_encrypted:
raise EncryptedPdfError() # Triggered by encryption with empty passwd raise EncryptedPdfError() # Triggered by encryption with empty passwd
self._pages = _pdf_pageinfo_concurrent( pscript5_mode = str(pdf.docinfo.get(Name.Creator, "")).startswith(
pdf, 'PScript5'
executor,
max_workers,
use_threads,
infile,
progbar,
check_pages=check_pages,
detailed_analysis=detailed_analysis,
) )
self._miner_state = (
PdfMinerState(infile, pscript5_mode)
if detailed_analysis
else nullcontext()
)
with self._miner_state as miner_state:
self._pages = _pdf_pageinfo_concurrent(
pdf,
executor,
max_workers,
use_threads,
infile,
progbar,
check_pages=check_pages,
detailed_analysis=detailed_analysis,
miner_state=miner_state,
)
self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False) self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False)
if Name.AcroForm in pdf.Root: if Name.AcroForm in pdf.Root:
if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0: if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0:
+69
View File
@@ -18,6 +18,7 @@ import pdfminer.encodingdb
import pdfminer.pdfdevice import pdfminer.pdfdevice
import pdfminer.pdfinterp import pdfminer.pdfinterp
import pdfminer.psparser import pdfminer.psparser
from deprecation import deprecated
from pdfminer.converter import PDFLayoutAnalyzer from pdfminer.converter import PDFLayoutAnalyzer
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
from pdfminer.pdfcolor import PDFColorSpace from pdfminer.pdfcolor import PDFColorSpace
@@ -289,6 +290,7 @@ def patch_pdfminer(pscript5_mode: bool):
yield yield
@deprecated(deprecated_in='16.6.0', details='Use PdfMinerState instead.')
def get_page_analysis( def get_page_analysis(
infile: PathLike, pageno: int, pscript5_mode: bool infile: PathLike, pageno: int, pscript5_mode: bool
) -> LTPage | None: ) -> LTPage | None:
@@ -319,6 +321,73 @@ def get_page_analysis(
return dev.get_result() return dev.get_result()
class PdfMinerState:
"""Provide a context manager for using pdfminer.six.
This ensures that the file is closed. It also provides a cache of pages
from the PDF so that they can be reused if needed, to improve performance.
"""
def __init__(self, infile: Path, pscript5_mode: bool) -> None:
"""Initialize the context manager.
Args:
infile: The path to the PDF file to be analyzed.
pscript5_mode: Whether the PDF was generated by PScript5.dll.
"""
self.infile = infile
self.rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
self.disable_boxes_flow = None
self.page_cache: list[PDFPage] = []
self.pscript5_mode = pscript5_mode
self.file = None
def __enter__(self):
"""Enter the context manager."""
self.file = Path(self.infile).open('rb')
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Exit the context manager."""
if self.file:
self.file.close()
return True
def _load_page_cache(self):
"""Load the page cache."""
try:
self.page_cache = list(PDFPage.get_pages(self.file))
if not self.page_cache:
raise InputFileError(
"pdfminer did not find any pages in the input file."
)
for n, page in enumerate(self.page_cache):
if page is None:
raise InputFileError(
f"pdfminer could not process page {n} (counting from 0)."
)
except PDFTextExtractionNotAllowed as e:
raise EncryptedPdfError() from e
def get_page_analysis(self, pageno: int):
"""Get the page analysis for a given page."""
if not self.page_cache:
self._load_page_cache()
page = self.page_cache[pageno]
dev = TextPositionTracker(
self.rman,
laparams=LAParams(
all_texts=True, detect_vertical=True, boxes_flow=self.disable_boxes_flow
),
)
interp = pdfminer.pdfinterp.PDFPageInterpreter(self.rman, dev)
with patch_pdfminer(self.pscript5_mode):
interp.process_page(page)
return dev.get_result()
def get_text_boxes(obj) -> Iterator[LTTextBox]: def get_text_boxes(obj) -> Iterator[LTTextBox]:
"""Get the text boxes attached to the current node.""" """Get the text boxes attached to the current node."""
for child in obj: for child in obj:
+4 -2
View File
@@ -215,8 +215,10 @@ to have {found_version}. Please update this program.
OLD_VERSION_REQUIRED_FOR = ''' OLD_VERSION_REQUIRED_FOR = '''
OCRmyPDF requires '{program}' {need_version} or higher when run with the OCRmyPDF requires '{program}' {need_version} or higher when run with the
{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to {required_for} arguments. {program} {found_version} is installed.
proceed. For best results, install the program.
If you omit these arguments, OCRmyPDF may be able to
proceed. For best results, update the program.
''' '''
OSX_INSTALL_ADVICE = ''' OSX_INSTALL_ADVICE = '''
-5
View File
@@ -24,11 +24,6 @@ def is_macos():
return platform.system() == 'Darwin' return platform.system() == 'Darwin'
def running_in_docker():
# Docker creates a file named /.dockerenv in all supported versions
return Path('/.dockerenv').exists()
def have_unpaper(): def have_unpaper():
try: try:
unpaper.version() unpaper.version()
+1 -1
View File
@@ -8,7 +8,7 @@ from subprocess import run
import pytest import pytest
from .conftest import running_in_docker from ocrmypdf.helpers import running_in_docker
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
running_in_docker(), running_in_docker(),
+1 -2
View File
@@ -13,8 +13,7 @@ import pytest
from packaging.version import Version from packaging.version import Version
from ocrmypdf import helpers from ocrmypdf import helpers
from ocrmypdf.helpers import running_in_docker
from .conftest import running_in_docker
needs_symlink = pytest.mark.skipif(os.name == 'nt', reason='needs posix symlink') needs_symlink = pytest.mark.skipif(os.name == 'nt', reason='needs posix symlink')
windows_only = pytest.mark.skipif(os.name != 'nt', reason="Windows test") windows_only = pytest.mark.skipif(os.name != 'nt', reason="Windows test")
+1 -1
View File
@@ -18,6 +18,7 @@ from PIL import Image
import ocrmypdf import ocrmypdf
from ocrmypdf._exec import tesseract from ocrmypdf._exec import tesseract
from ocrmypdf.exceptions import ExitCode, MissingDependencyError from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.helpers import running_in_docker
from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
from ocrmypdf.subprocess import get_version from ocrmypdf.subprocess import get_version
@@ -29,7 +30,6 @@ from .conftest import (
is_macos, is_macos,
run_ocrmypdf, run_ocrmypdf,
run_ocrmypdf_api, run_ocrmypdf_api,
running_in_docker,
) )
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name
+2 -1
View File
@@ -208,12 +208,13 @@ def test_pages_issue700(monkeypatch, resources):
monkeypatch.setattr(PDFPage, 'get_pages', get_no_pages) monkeypatch.setattr(PDFPage, 'get_pages', get_no_pages)
with pytest.raises(InputFileError, match="pdfminer"): with pytest.raises(InputFileError, match="pdfminer"):
pdfinfo.PdfInfo( pi = pdfinfo.PdfInfo(
resources / 'cardinal.pdf', resources / 'cardinal.pdf',
detailed_analysis=True, detailed_analysis=True,
progbar=False, progbar=False,
max_workers=1, max_workers=1,
) )
pi._miner_state.get_page_analysis(0)
@pytest.fixture @pytest.fixture
+3 -1
View File
@@ -239,7 +239,9 @@ def make_rotate_test(imagefile, outdir, prefix, image_angle, page_angle):
@pytest.mark.parametrize('image_angle', (0, 90, 180, 270)) @pytest.mark.parametrize('image_angle', (0, 90, 180, 270))
def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog): def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
reference = make_rotate_test(resources / 'typewriter.png', outdir, 'ref', 0, 0) reference = make_rotate_test(resources / 'typewriter.png', outdir, 'ref', 0, 0)
test = make_rotate_test(resources, outdir, 'test', image_angle, page_angle) test = make_rotate_test(
resources / 'typewriter.png', outdir, 'test', image_angle, page_angle
)
out = test.with_suffix('.out.pdf') out = test.with_suffix('.out.pdf')
exitcode = run_ocrmypdf_api( exitcode = run_ocrmypdf_api(
+14 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging import logging
from os import fspath from os import fspath
from unittest.mock import patch from unittest.mock import Mock, patch
import pytest import pytest
from packaging.version import Version from packaging.version import Version
@@ -48,6 +48,19 @@ def test_old_unpaper(resources, no_outpdf):
mock.assert_called() mock.assert_called()
def test_unpaper_version_chatter(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.subprocess.run") as mock:
mock.return_value = Mock(stdout='Warning: using insecure memory!\n7.0.0\n')
with pytest.raises(MissingDependencyError):
check_options(options, pm)
mock.assert_called()
@needs_unpaper @needs_unpaper
def test_clean(resources, outpdf): def test_clean(resources, outpdf):
check_ocrmypdf( check_ocrmypdf(