Compare commits

...
85 Commits
Author SHA1 Message Date
James R. Barlow f072e91120 v14.0.2 2023-01-03 17:07:53 -08:00
James R. Barlow efa2bca8a3 Github Actions ignores ||, force it to do the right thing 2023-01-03 14:51:09 -08:00
James R. Barlow b039010d3e Ignore brew update errors 2023-01-03 14:36:21 -08:00
James R. Barlow 3f7cefcf5d macos - drop Python 3.10 since homebrew is having trouble 2023-01-02 13:08:42 -08:00
James R. Barlow 45f97d1565 macos build: brew first? 2023-01-02 12:36:28 -08:00
James R. Barlow 1281f8eb68 Workaround 2to3 linking error in macos build 2023-01-02 01:10:20 -08:00
James R. Barlow 9ef61864fb Update Python versions 2023-01-01 19:13:50 -08:00
James R. Barlow 90b2119ad3 pdfinfo: skip null FormX entries 2023-01-01 16:58:14 -08:00
James R. Barlow f0cc7f2230 docs: explicitly end win32 support 2023-01-01 16:58:14 -08:00
M.Yasoob Ullah Khalid ☺andGitHub d60a384aab Fixed the source installation instructions (#1048) 2022-12-22 02:54:53 -08:00
James R. Barlow 14a6093636 Remove old issue templates 2022-12-14 17:33:00 -08:00
James R. Barlow 54b42d73ab Merge remote-tracking branch 'origin/master' 2022-12-14 17:31:56 -08:00
M.Yasoob Ullah Khalid ☺andGitHub 9abed14f1f fixed interchanged words (#1039) 2022-12-14 17:24:55 -08:00
Doug RinckesandGitHub d09f61d4fe log completion message (#1044)
This logs the "done" message if neither delete nor archive options are set.
2022-12-14 17:24:41 -08:00
M.Yasoob Ullah Khalid ☺andGitHub 4a0130649e Fixed some wording (#1045) 2022-12-14 17:24:22 -08:00
jbarlow83andGitHub a0224d94ca Update issue templates 2022-12-14 17:23:57 -08:00
Carlos GarcésandGitHub 9e7b9de830 Fix tesseract documentation url (#1047) 2022-12-14 11:58:21 -08:00
James R. Barlow 08fc5fc01d Fix issue templates - Github doesn't support comments above 2022-10-24 14:55:08 -07:00
James R. Barlow 110c75cba2 Use actual TesseractVersion for comparison 2022-10-03 23:28:36 -07:00
James R. Barlow 5afca3f342 Use relaxed version parsing 2022-10-03 22:44:35 -07:00
James R. Barlow d9eb0ba7ef v14.0.1 release notes 2022-10-03 22:02:50 -07:00
James R. Barlow 46d0978a09 Update version scripts to support Ghostscript 10.0 2022-10-03 21:59:31 -07:00
James R. Barlow 4e35100978 Docker image: add jbig2dec 2022-10-03 20:08:28 -07:00
James R. Barlow 7bd0e43243 Merge branch 'feature/py311' 2022-09-21 02:42:57 -07:00
James R. Barlow 9cd97da5f2 dockerfile: toml 2022-09-21 02:20:28 -07:00
James R. Barlow d002703c41 docker: install gpg-agent 2022-09-21 01:04:34 -07:00
James R. Barlow b1fecf3b05 Temporarily disable PyPy 3.8 and 3.9 testing
Minor pikepdf issue?
2022-09-21 01:00:08 -07:00
James R. Barlow c2ccc7f29d Fix test failure due to new logging from pikepdf 2022-09-21 01:00:08 -07:00
James R. Barlow 36dfd12e2c Enable Python 3.11 testing on Linux 2022-09-21 00:59:44 -07:00
James R. Barlow 7da4e6ca7f Address some linter warnings 2022-09-21 00:05:12 -07:00
James R. Barlow 16fc52079d Drop setup.py - use pyproject.toml exclusively 2022-09-20 23:47:40 -07:00
James R. Barlow f37decf3b3 Remove old pdfminer.six shim 2022-09-20 23:02:17 -07:00
James R. Barlow 4ad4a13ef0 Simplify dependencies 2022-09-20 22:50:44 -07:00
James R. Barlow 6f61f69a8d v14.0.0 release notes 2022-09-20 22:47:07 -07:00
James R. Barlow 88831e8ab1 Merge branch 'feature/tess5' 2022-09-20 22:33:07 -07:00
James R. Barlow 2ebc36fcec Merge branch 'feature/drop-3.7' 2022-09-20 22:32:59 -07:00
James R. Barlow 1709e23701 pipeline: add further typing details 2022-09-19 20:19:15 -07:00
James R. Barlow 2e55cb5980 helpers: improve typing of Resolution 2022-09-19 20:03:54 -07:00
James R. Barlow 6dbaebdc0c Merge branch 'master' into feature/drop-3.7 2022-09-15 23:00:27 -07:00
James R. Barlow 5156fe7662 Test PyPy 3.8 and 3.9 2022-09-15 22:56:44 -07:00
James R. Barlow 2c99c89e45 ghostscript: document why we treat gs 9.56.0 differently 2022-09-15 22:51:04 -07:00
James R. Barlow 74286e7e1e Use tesseract 5 again in Docker image 2022-08-23 13:39:45 -07:00
James R. Barlow 2e937dee9f Refactor cache manifest creation 2022-08-19 00:19:38 -07:00
James R. Barlow 23f3830533 Merge remote-tracking branch 'origin/master' 2022-08-14 17:01:10 -07:00
belamuandGitHub 305e6594be add Fedora packages in jbig2 doc (#1006)
Packages needed to install jbig2 encoding are mentioned for Ubuntu systems but are missing for other systems. This commit adds the infomation in the documentation what the packages are called on Fedora systems.
2022-08-11 10:31:09 -07:00
James R. Barlow f4155dca77 tests: convert all uses of multipage.pdf to fixture 2022-08-11 01:13:10 -07:00
James R. Barlow 545cd031b0 Replace public domain graph.pdf and derivates with licensed version 2022-08-11 01:09:00 -07:00
James R. Barlow a1c7826336 dep5: mention mawi 2022-08-06 15:47:45 -07:00
James R. Barlow c5359bd990 jbig2 is from linn now 2022-08-06 15:36:20 -07:00
James R. Barlow 7f77308846 Remake palette.pdf using baiona-colormapped file 2022-08-06 15:35:47 -07:00
James R. Barlow 8e7c5f3001 dep5: correction, graph.pdf is actually public domain 2022-08-06 15:34:38 -07:00
James R. Barlow 79db985181 Improve encryption tests; drop some public domain resources
Generate the encrypted files we need and remove special test files we retained for this.

Replace jbig2.pdf based on congress.jpg with version based on ccitt.pdf.
2022-08-06 14:37:45 -07:00
James R. Barlow 7d23a661fc Track .reuse/dep5 2022-08-06 14:32:09 -07:00
James R. Barlow 99e94807c5 .gitignore: updates 2022-08-06 14:30:05 -07:00
James R. Barlow 8412de9344 Merge branch 'master' into feature/drop-3.7 2022-08-06 14:04:23 -07:00
James R. Barlow b458b1422b Merge remote-tracking branch 'origin/master' 2022-08-06 03:06:07 -07:00
James R. Barlow 76bd8cab13 Drop Ubuntu 18.04 content 2022-08-06 02:58:26 -07:00
James R. Barlow ef70c9499e Don't deprecate falsy filter_page_image 2022-08-05 01:07:46 -07:00
James R. Barlow 47dcb6fcd0 Revert "Remove deprecated falsy handling of filter_page_image"
This reverts commit c9389c7713.
2022-08-05 01:01:09 -07:00
James R. Barlow 88d2949e6b Abolish setup.cfg and migrate to pyproject.toml 2022-08-04 05:00:46 -07:00
James R. Barlow c9389c7713 Remove deprecated falsy handling of filter_page_image 2022-08-04 04:21:15 -07:00
James R. Barlow 4d2f499f97 Remove optional status of coloredlogs
Everything optional is a possible complication.
Better to remove the option.
2022-08-04 04:15:56 -07:00
James R. Barlow 4104904a1e Document reason for suppress some third party deprecation warnings 2022-08-04 04:13:02 -07:00
James R. Barlow 1a0a797ca6 Remove our @deprecated decorator and use standard package 2022-08-04 04:00:25 -07:00
James R. Barlow 670ce2b969 Remove support for non-callable version checker 2022-08-04 03:58:10 -07:00
James R. Barlow 7e97981114 hocrtx: unused imports 2022-08-04 03:56:44 -07:00
James R. Barlow 53db866ef9 Remove deprecated exception PdfMergeFailedError 2022-08-04 03:54:55 -07:00
James R. Barlow d591a3e059 resources readme: remove license and copyright info
Better to not repeat ourselves and present this info in example one place.
2022-08-04 03:42:22 -07:00
James R. Barlow 37c050aa4f readme: fix copyright 2022-08-04 03:41:45 -07:00
James R. Barlow 4b9ea40a0c spdx: move identifiers to files that support them
If the apparent license changed, take this commit as correct.
2022-08-04 03:26:54 -07:00
林博仁(Buo-ren, Lin)andGitHub 165432486b snap: add JBIG2 support (#1001)
This patch includes a jbig2enc build into the snap package, allow the
user to run OcrMyPDF with the --jbig2-lossy option or other options that trigger JBIG2 encoding.

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2022-08-02 16:35:01 -07:00
James R. Barlow d619fac0bd unpaper: tidy file 2022-08-02 15:30:20 -07:00
James R. Barlow acc70036cc Set minimum Tesseract to 4.1.1 2022-08-02 15:20:29 -07:00
James R. Barlow 80b7cf6330 Update black target versions 2022-08-02 15:07:43 -07:00
James R. Barlow 8a8c06c79c Update pre-commit for py3.8+ 2022-08-02 15:05:43 -07:00
James R. Barlow 67773da309 Drop support for Ghostscript <9.50 2022-08-02 15:01:10 -07:00
James R. Barlow d5a9861d5c readme: freebsd calls it py-ocrmypdf now 2022-08-02 14:49:09 -07:00
James R. Barlow 9ffe829a10 Remove external importlib_metadata since Python 3.8 provides it directly 2022-08-02 14:48:11 -07:00
James R. Barlow 8a3b82e364 Make Python 3.8 minimum requirement 2022-08-02 14:46:01 -07:00
James R. Barlow 580822a6a2 Fix Windows ghostscript path scanning 2022-08-02 14:39:23 -07:00
James R. Barlow 9f3a52fd12 windows: compare Ghostscript versions properly 2022-08-01 13:58:24 -07:00
James R. Barlow 52e829d845 windows: tidy path shimming 2022-08-01 13:23:57 -07:00
James R. Barlow 2b2e5c271a Don't lru_cache versions
lru_cache could interfere with testing and we don't have evidence this
is slow enough to justify optimizing.
2022-08-01 13:23:31 -07:00
James R. Barlow 5fe3102e4e tests: new test to confirm correct printing of tesseract install advice 2022-08-01 12:31:37 -07:00
James R. Barlow 5b57520c98 tests: simplify some validation tests 2022-08-01 12:31:05 -07:00
76 changed files with 714 additions and 753 deletions
+9 -3
View File
@@ -1,5 +1,5 @@
# OCRmyPDF
#
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
FROM ubuntu:22.04 as base
@@ -50,8 +50,14 @@ RUN pip3 install --no-cache-dir .[test,webservice,watcher]
FROM base
# For Tesseract 5
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 apt-get update && apt-get install -y --no-install-recommends \
ghostscript \
jbig2dec \
img2pdf \
libsm6 libxext6 libxrender-dev \
pngquant \
@@ -74,7 +80,7 @@ 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/setup.cfg /app/setup.py /app/README.md /app/
COPY --from=builder /app/pyproject.toml /app/README.md /app/
COPY --from=builder /app/tests /app/tests
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# dotfiles
.*
!.coveragerc
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Always use Unix convention for new lines
* text eol=lf
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
@@ -17,11 +17,7 @@ A clear and concise description of what you want to happen.
A clear and concise description of any alternative solutions or features you've considered. Please include the versions of OCRmyPDF and other supporting programs (Tesseract OCR, Ghostscript) - maybe an alternative already exists in a newer version.
**Example file**
If your issue concerns how OCRmyPDF processes certain files, and please provide an example file that helps illustrate how OCRmyPDF's output could be improve.
Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/ocrmypdf/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.
If your issue concerns how OCRmyPDF processes certain files, and please provide an example file that helps illustrate how OCRmyPDF's output could be improve. You could also look in ``tests/resources`` and see if any of those files demonstrates your issue.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,7 +1,7 @@
---
name: General issues
about: Installation, packages, dependencies, "nothing works", test suite failures...
title: ''
title: "[BUG]"
labels: ''
assignees: ''
@@ -23,6 +23,7 @@ If applicable, add screenshots to help explain your problem.
- OS:
- Python version:
- OCRmyPDF version:
- Platform: x64 or ARM
**Installation**
How did you install OCRmyPDF? Did you install it from your operating system's
@@ -1,7 +1,7 @@
---
name: Problem with a specific input file
name: Problem with specific file
about: Something went wrong while trying to OCR a specific file
title: ''
title: "[BUG]"
labels: ''
assignees: ''
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
+22 -22
View File
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
name: Test and deploy
on:
@@ -20,8 +22,6 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-18.04
python: "3.7"
- os: ubuntu-20.04
python: "3.8"
- os: ubuntu-20.04
@@ -31,7 +31,13 @@ jobs:
- os: ubuntu-latest
python: "3.9"
- os: ubuntu-latest
python: "pypy-3.8"
python: "3.10"
- os: ubuntu-latest
python: "3.11"
# - os: ubuntu-latest
# python: "pypy3.8"
#- os: ubuntu-latest
# python: "pypy3.9"
- os: ubuntu-latest
python: "3.9"
tesseract5: true
@@ -73,12 +79,6 @@ jobs:
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' || matrix.os == 'ubuntu-latest'
run: |
@@ -96,7 +96,7 @@ jobs:
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
python -m pip install --prefer-binary .[test]
- name: Report versions
run: |
@@ -122,7 +122,7 @@ jobs:
strategy:
matrix:
os: [macos-latest]
python: ["3.9", "3.10"]
python: ["3.10", "3.11"]
env:
OS: ${{ matrix.os }}
@@ -133,12 +133,8 @@ jobs:
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install Homebrew deps
continue-on-error: true
run: |
brew update
brew install \
@@ -149,10 +145,15 @@ jobs:
pngquant \
tesseract
- uses: actions/setup-python@v4
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
python -m pip install --prefer-binary .[test]
- name: Report versions
run: |
@@ -177,7 +178,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python: ["3.9", "3.10"]
python: ["3.10", "3.11"]
env:
OS: ${{ matrix.os }}
@@ -201,7 +202,7 @@ jobs:
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
python -m pip install --prefer-binary .[test]
- name: Test
run: |
@@ -228,9 +229,8 @@ jobs:
- name: Make wheels and sdist
run: |
python -m pip install --upgrade pip wheel
python setup.py sdist
python setup.py bdist_wheel
python -m pip install --upgrade pip wheel build
python -m build --sdist --wheel
- uses: actions/upload-artifact@v3
with:
+10 -11
View File
@@ -1,14 +1,14 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: CC-BY-SA-4.0
# dotfiles
.*
!.coveragerc
!.dockerignore
!.git_archival.txt
!.gitattributes
!.gitignore
!.pre-commit-config.yaml
!.readthedocs.yaml
!.github/
!.docker/
.coverage
.venv*/
.tox/
.vscode/
.ipynb_checkpoints/
.mypy_cache/
.pytest_cache/
# Dev scratch
*.ipynb
@@ -42,4 +42,3 @@ docs/_build/
docs/_static/
docs/_templates/
docs/Makefile
ocrmypdf/lib/_*.py
+4 -1
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
@@ -25,7 +28,7 @@ repos:
rev: v2.37.2
hooks:
- id: pyupgrade
args: ["--py37-plus"]
args: ["--py38-plus"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
hooks:
+4 -1
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
@@ -14,7 +17,7 @@ formats:
# Optionally set the version of Python and requirements required to build your docs
python:
version: "3.7"
version: "3.8"
install:
- method: pip
path: .
+132
View File
@@ -0,0 +1,132 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: OCRmyPDF
Upstream-Contact: James R. Barlow <james@purplerock.ca>
Source: https://github.com/ocrmypdf/OCRmyPDF
Files:
.git_archival.txt
docs/images/logo-social.png
docs/images/logo-square-256.svg
docs/images/logo-square.png
docs/images/logo-square.svg
docs/images/logo.svg
setup.cfg
Copyright: (C) 2022 James R. Barlow
License: MPL-2.0
Files:
.github/ISSUE_TEMPLATE/*.md
docs/images/macos-workflow.png
Copyright: (C) 2022 James R. Barlow
License: CC-BY-SA-4.0
Files:
tests/resources/acroform.pdf
tests/resources/aspect.pdf
tests/resources/blank.pdf
tests/resources/cmyk.pdf
tests/resources/crom.png
tests/resources/enormous.pdf
tests/resources/formxobject.pdf
tests/resources/francais.pdf
tests/resources/hugemono.pdf
tests/resources/invalid.pdf
tests/resources/kcs.pdf
tests/resources/livecycle.pdf
tests/resources/missing_docinfo.pdf
tests/resources/negzero.pdf
tests/resources/no_contents.pdf
tests/resources/toc.pdf
tests/resources/trivial.pdf
tests/resources/truetype_font_nomapping.pdf
tests/resources/type3_font_nomapping.pdf
Copyright: (C) 2022 James R. Barlow
License: CC-BY-SA-4.0
Files:
tests/resources/graph.pdf
tests/resources/graph_ocred.pdf
Copyright: (C) 2012 SmokeyJoe
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/c02-22.pdf
tests/resources/congress.jpg
tests/resources/multipage.pdf
Copyright: Public domain
License: public-domain
Copyright on these files has expired.
Files: docs/images/bitmap_vs_svg.svg
Copyright: (C) 2006 Yug
License: CC-BY-SA-2.5
Files: tests/cache/*
Copyright: (C) 2022 James R. Barlow
License: CC-BY-SA-4.0
Files: tests/resources/linn.png
tests/resources/linn.pdf
tests/resources/linn.txt
tests/resources/ccitt.pdf
tests/resources/cardinal.pdf
tests/resources/jbig2.pdf
tests/resources/skew.pdf
tests/resources/rotated_skew.pdf
tests/resources/poster.pdf
Copyright: (C) 1985 Forat Electronics
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/lichtenstein.pdf
Copyright: (C) 2001 Andreas Tille
(C) 2007 Alessio Damato
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/masks.pdf
Copyright: held by the contributors to the German Wikipedia article "Linux"
see: https://de.wikipedia.org/w/index.php?title=Linux&action=history
(masks.pdf generated from Wikipedia article as of 2016-08-24)
License: CC-BY-SA-3.0
Files: tests/resources/epson.pdf
Copyright: held by the contributors to the Wikipedia article "Optical character recognition"
see: https://en.wikipedia.org/w/index.php?title=Optical_character_recognition&action=history
(epson.pdf generated from Wikipedia article as of 2016-09-14)
License: CC-BY-SA-3.0
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf
Copyright: (C) 2005 Ellywa
License: GFDL-1.2-or-later 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: MIT
Files:
tests/resources/baiona*.png
tests/resources/baiona*.jpg
tests/resources/link.pdf
tests/resources/palette.pdf
Copyright: (C) 2014 Euskaldunaa
License: CC-BY-SA-4.0
Files: tests/resources/vector.pdf
Copyright: (C) 2018 Catscratch
License: MIT
Files: src/ocrmypdf/data/sRGB.icc
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
Marti Maria <www.littlecms.com>
Photogamut <www.photogamut.org>
Graeme Gill <www.argyllcms.com>
ColorSolutions <www.basICColor.com>
License: Zlib
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-later 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
+6 -5
View File
@@ -1,3 +1,7 @@
<!-- SPDX-FileCopyrightText: 2014 Julien Pfefferkorn -->
<!-- SPDX-FileCopyrightText: 2015 James R. Barlow -->
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
[![Build Status](https://github.com/ocrmypdf/OCRmyPDF/actions/workflows/build.yml/badge.svg)](https://github.com/ocrmypdf/OCRmyPDF/actions/workflows/build.yml) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
@@ -66,7 +70,7 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
| macOS (Homebrew) | ``brew install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py37-ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
| Conda | ``conda install ocrmypdf`` |
| Ubuntu Snap | ``snap install ocrmypdf`` |
@@ -92,10 +96,7 @@ 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.
OCRmyPDF supports Tesseract 4.0 and the beta versions of Tesseract 5.0. It will
automatically use whichever version it finds first on the `PATH` environment
variable. On Windows, if `PATH` does not provide a Tesseract binary, we use
the highest version number that is installed according to the Windows Registry.
OCRmyPDF supports Tesseract 4.1.1+. It will automatically use whichever version it finds first on the `PATH` environment variable. On Windows, if `PATH` does not provide a Tesseract binary, we use the highest version number that is installed according to the Windows Registry.
## Documentation and support
+1 -1
View File
@@ -160,7 +160,7 @@ Changing tesseract configuration variables
------------------------------------------
You can override tesseract's default `control
parameters <https://github.com/tesseract-ocr/tesseract/wiki/ControlParams>`__
parameters <https://tesseract-ocr.github.io/tessdoc/tess3/ControlParams.html>`__
with a configuration file.
As an example, this configuration will disable Tesseract's dictionary
-8
View File
@@ -72,14 +72,6 @@ OCRmyPDF, use processes.
not take at least one of these steps, process semantics will prevent
OCRmyPDF from working correctly.
.. warning::
On macOS with Python 3.7, you must call
:func:`multiprocessing.set_start_method("spawn")`. Without this, multiprocessing
will be unstable. From the command line, OCRmyPDF does this automatically,
but as an API user you must do this. See Python bpo-33725 for details.
Python 3.8+ also resolve this automatically.
Logging
-------
+2 -2
View File
@@ -21,8 +21,8 @@ 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/``
This command will run ``ocrmypdf`` on all files named ``*.pdf`` in the
current directory and write them to the previously created ``output/``
folder. It will not search subdirectories.
The ``--tag`` argument tells parallel to print the filename as a prefix
+2 -4
View File
@@ -76,6 +76,8 @@ author = 'James R. Barlow'
# The short X.Y version.
import os
from importlib.metadata import version as package_version
on_rtd = os.environ.get('READTHEDOCS') == 'True'
@@ -96,10 +98,6 @@ if on_rtd:
]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
try:
from importlib_metadata import version as package_version
except ModuleNotFoundError:
from importlib.metadata import version as package_version
# The full version, including alpha/beta/rc tags.
release = package_version('ocrmypdf')
+1 -1
View File
@@ -283,7 +283,7 @@ 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
improvements in Tesseract for files they previously OCRed with an
earlier version of Tesseract and OCRmyPDF.
.. code-block:: bash
+18 -59
View File
@@ -44,7 +44,7 @@ install, or install a more recent version than your platform provides, read on.
Installing on Linux
===================
Debian and Ubuntu 18.04 or newer
Debian and Ubuntu 20.04 or newer
--------------------------------
.. |deb-11| image:: https://repology.org/badge/version-for-repo/debian_11/ocrmypdf.svg
@@ -56,9 +56,6 @@ Debian and Ubuntu 18.04 or newer
.. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg
:alt: Debian unstable
.. |ubu-1804| image:: https://repology.org/badge/version-for-repo/ubuntu_18_04/ocrmypdf.svg
:alt: Ubuntu 18.04 LTS
.. |ubu-2004| image:: https://repology.org/badge/version-for-repo/ubuntu_20_04/ocrmypdf.svg
:alt: Ubuntu 20.04 LTS
@@ -72,7 +69,7 @@ Debian and Ubuntu 18.04 or newer
+-----------------------------------------------+
| |deb-11| |deb-12| |deb-unstable| |
+-----------------------------------------------+
| |ubu-1804| |ubu-2004| |ubu-2204| |
| |ubu-2004| |ubu-2204| |
+-----------------------------------------------+
Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users
@@ -80,7 +77,7 @@ of Windows Subsystem for Linux, may simply
.. code-block:: bash
apt-get install ocrmypdf
apt 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
@@ -198,46 +195,6 @@ To install for the current user only:
To add JBIG2 encoding, see :ref:`jbig2`.
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 \
libxml2 \
pngquant \
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`.
Arch Linux (AUR)
----------------
@@ -417,9 +374,9 @@ Native Windows
You must install the following for Windows:
* Python 3.7 (64-bit) or later
* Tesseract 4.0 or later
* Ghostscript 9.50 or later
* Python 3.8 (64-bit) or later
* Tesseract 4.1.1 (64-bit) or later
* Ghostscript 9.50 (64-bit) or later
Using the `Chocolatey <https://chocolatey.org/>`_ package manager, install the
following when running in an Administrator command prompt:
@@ -438,10 +395,8 @@ 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.
Chocolatey automatically selects appropriate versions of these applications. Please make sure
you are installing the 64-bit versions.
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
for third party software it needs (specifically, Tesseract and Ghostscript). To
@@ -456,6 +411,10 @@ to change the PATH.
Please download Python from Python.org or Chocolatey instead, and do not use the
Microsoft Store version.
.. warning::
32-bit Windows might work, but is not supported.
Windows Subsystem for Linux
---------------------------
@@ -481,7 +440,7 @@ Cygwin64
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
python37 (or later)
python38 (or later)
python3?-devel
python3?-pip
python3?-lxml
@@ -616,9 +575,9 @@ manager. ``pip`` cannot provide them.
The following versions are required:
- Python 3.7 or newer
- Ghostscript 9.23 or newer
- Tesseract 4.0.0 or newer
- Python 3.8 or newer
- Ghostscript 9.50 or newer
- Tesseract 4.1.1 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
- unpaper 6.1
@@ -649,7 +608,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
Installing HEAD revision from sources
=====================================
If you have ``git`` and Python 3.7 or newer installed, you can install
If you have ``git`` and Python 3.8 or newer installed, you can install
from source. When the ``pip`` installer runs, it will alert you if
dependencies are missing.
@@ -679,7 +638,7 @@ system-wide:
.. code-block:: bash
git clone -b master https://github.com/ocrmypdf/OCRmyPDF.git
python3 -m venv
python3 -m venv venv
source venv/bin/activate
cd OCRmyPDF
pip install .
+2 -3
View File
@@ -85,7 +85,7 @@ 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 <http://ghostscript.com/>`__ to rasterize the page, and
then performs on OCR the rasterized image to create an OCR "layer".
then performs 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
@@ -190,8 +190,7 @@ Ghostscript also imposes some limitations:
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.)
(Modern Ghostscript can copy JPEG images without transcoding them.)
- 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.
+2 -1
View File
@@ -37,7 +37,8 @@ For all other Linux, you must build a JBIG2 encoder from source:
.. _jbig2-lossy:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev.
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
as libtool and leptonica-devel.
Lossy mode JBIG2
================
+6 -10
View File
@@ -76,20 +76,16 @@ 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
Your package's ``pyproject.toml`` would need to contain the following, for a plugin
named ``ocrmypdf-exampleplugin``:
.. code-block:: python
.. code-block:: toml
# sample ./setup.py file
from setuptools import setup
[project]
name = "ocrmypdf-exampleplugin"
setup(
name="ocrmypdf-exampleplugin",
packages=["exampleplugin"],
# the following makes a plugin available to pytest
entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]},
)
[project.entry-points."ocrmypdf"]
exampleplugin = "exampleplugin.pluginmodule"
.. code-block:: ini
+30
View File
@@ -28,6 +28,36 @@ tagged yet.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v14.0.2
=======
- Fixed :issue:`1052`, an exception on attempting to process certain nonconforming PDFs.
- Explicitly documented that Windows 32-bit is no longer supported.
- Fixed source installation instructions.
- Other documentation fixes.
v14.0.1
=======
- Fixed some version checks done with smart version comparison.
- Added missing jbig2dec to Docker image.
v14.0.0
=======
- Dropped support for Python 3.7.
- Dropped support generally speaking, all dependencies older than what Ubuntu 20.04
provides.
- Ghostscript 9.50 or newer is now required. Shims to support old versions were
removed.
- Tesseract 4.1.1 or newer is now required. Shims to support old versions were
removed.
- Docker image now uses Tesseract 5.
- Dropped setup.cfg configuration for pyproject.toml.
- Removed deprecation exception PdfMergeFailedError.
- A few more public domain test files were removed or replaced. We are aiming for
100% compliance with SPDX and generally towards simplifying copyright.
v13.7.0
=======
+2
View File
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MIT
---
version: "3.3"
services:
+3 -4
View File
@@ -2,6 +2,8 @@
# SPDX-FileCopyrightText: 2017 Enantiomerie
# SPDX-License-Identifier: MIT
"""Example OCRmyPDF for Synology NAS"""
from __future__ import annotations
# This script must be edited to meet your needs.
@@ -25,10 +27,7 @@ logging.basicConfig(
filemode='w',
)
if len(sys.argv) > 1:
start_dir = sys.argv[1]
else:
start_dir = '.'
start_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
for dir_name, _subdirs, file_list in os.walk(start_dir):
logging.info(dir_name)
+2
View File
@@ -103,6 +103,8 @@ def execute_ocrmypdf(file_path):
elif ON_SUCCESS_ARCHIVE:
log.info(f'OCR is done. Archiving {file_path.name} to {ARCHIVE_DIRECTORY}')
shutil.move(file_path, f'{ARCHIVE_DIRECTORY}/{file_path.name}')
else:
log.info('OCR is done')
else:
log.info('OCR is done')
+89 -4
View File
@@ -1,16 +1,102 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
[build-system]
requires = [
"setuptools >= 52",
"setuptools >= 61",
"setuptools_scm[toml] >= 7.0.5",
"wheel"
]
build-backend = "setuptools.build_meta"
[project]
name = "ocrmypdf"
dynamic = ["version"]
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
readme = "README.md"
license = {text = "MPL-2.0"}
requires-python = ">=3.8"
dependencies = [
"Pillow>=8.2.0",
"coloredlogs>=14.0",
"deprecation>=2.1.0",
"img2pdf>=0.3.0", # pure Python
"packaging>=20",
"pdfminer.six>=20201018",
"pikepdf>=5.0.1",
"pluggy>=0.13.0",
"reportlab>=3.5.66",
"tqdm>=4",
"importlib-resources>=5;python_version<'3.9'", # until Python 3.9
"typing-extensions>=4;python_version<'3.10'",
]
authors = [{name = "James R. Barlow", email="james@purplerock.ca"}]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"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",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Scientific/Engineering :: Image Recognition",
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
]
keywords = [
"PDF",
"OCR",
"optical character recognition",
"PDF/A",
"scanning",
]
[project.urls]
Documentation = "https://ocrmypdf.readthedocs.io/"
Source = "https://github.com/ocrmypdf/OCRmyPDF"
Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
[project.optional-dependencies]
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
extended_test = ["PyMuPDF==1.19.1"]
test = [
"coverage[toml]>=5",
"pytest>=6.0.0",
"pytest-cov>=2.11.1",
"pytest-xdist>=2.2.0",
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
"types-Pillow",
"types-humanfriendly",
]
watcher = ["watchdog>=1.0.2"]
webservice = ["Flask>=1"]
[project.scripts]
ocrmypdf = "ocrmypdf.__main__:run"
[tool.setuptools.package-data]
ocrmypdf = ["data/sRGB.icc", "py.typed"]
[tool.setuptools.packages.find]
where = ["src"]
namespaces = false
[tool.setuptools_scm]
[tool.distutils.bdist_wheel]
python-tag = "py38"
[tool.black]
line-length = 88
target-version = ["py37", "py38"]
target-version = ["py38", "py39", "py310", "py311"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
@@ -94,8 +180,7 @@ module = [
'pdfminer.*',
'reportlab.*',
'fitz',
'libxmp.utils',
'importlib_metadata'
'libxmp.utils'
]
ignore_missing_imports = true
-116
View File
@@ -1,116 +0,0 @@
[metadata]
name = ocrmypdf
description = OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/ocrmypdf/OCRmyPDF
author = James R. Barlow
author_email = james@purplerock.ca
license = MPL-2.0
license_file = LICENSE
license_files =
LICENSE
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: End Users/Desktop
Intended Audience :: Science/Research
Intended Audience :: System Administrators
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
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Scientific/Engineering :: Image Recognition
Topic :: Text Processing :: Indexing
Topic :: Text Processing :: Linguistic
keywords =
PDF
OCR
optical character recognition
PDF/A
scanning
project_urls =
Documentation = https://ocrmypdf.readthedocs.io/
Source = https://github.com/ocrmypdf/OCRmyPDF
Tracker = https://github.com/ocrmypdf/OCRmyPDF/issues
[options]
packages = find:
install_requires =
Pillow>=8.2.0
coloredlogs>=14.0 # strictly optional
img2pdf>=0.3.0 # pure Python
packaging>=20
pdfminer.six!=20200720,>=20191110
pikepdf!=5.0.0,>=4.0.0
pluggy>=0.13.0
reportlab>=3.5.66
tqdm>=4
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
importlib-resources>=5;python_version<'3.9' # until Python 3.9
typing-extensions>=4;python_version<'3.10'
python_requires = >=3.7
include_package_data = True
package_dir =
=src
platforms = any
setup_requires =
setuptools-scm
setuptools-scm-git-archive
zip_safe = False
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
ocrmypdf = ocrmypdf.__main__:run
[options.extras_require]
docs =
sphinx
sphinx-issues
sphinx-rtd-theme
extended_test =
PyMuPDF==1.19.1
test =
coverage[toml]>=5
pytest>=6.0.0
pytest-cov>=2.11.1
pytest-xdist>=2.2.0
python-xmp-toolkit==2.0.1 # also requires apt-get install libexempi3
types-Pillow
types-humanfriendly
watcher =
watchdog>=1.0.2
webservice =
Flask>=1
[options.package_data]
ocrmypdf =
data/sRGB.icc
py.typed
[bdist_wheel]
python-tag = py37
[aliases]
test = pytest
[check-manifest]
ignore =
.github
[flake8]
ignore = D203,F401,W503,E501,E203,F841
exclude = .git,__pycache__,docs/conf.py,build,dist,.venv,.venvpp,.eggs,tmp,src/ocrmypdf/lib/
max-complexity = 10
max-line-length = 100
-10
View File
@@ -1,10 +0,0 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""setup.py to support older setuptools and pip."""
from __future__ import annotations
from setuptools import setup
setup()
+11
View File
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2022 Alexander Langanke
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
name: ocrmypdf
title: OCRmyPDF
base: core20
@@ -41,6 +45,13 @@ parts:
stage-packages:
- lib32stdc++6
jbig2enc:
plugin: autotools
source: https://github.com/agl/jbig2enc.git
source-tag: '0.29'
build-packages:
- libleptonica-dev
ocrmypdf:
plugin: python
source: https://github.com/ocrmypdf/OCRmyPDF.git
+5 -2
View File
@@ -1,3 +1,6 @@
<!-- SPDX-FileCopyrightText: 2022 James R. Barlow -->
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# Release checklist
## Patch release
@@ -14,11 +17,11 @@
- Check README.md
- Check setup.py
- Check pyproject.toml
- Are classifiers up to date?
- Is `python_requires` correct?
- Python 3.6 is EOL on December 2021-12. Could drop support then.
- Is it to drop support for older Pythons?
- Can we tighten any `install_requires` dependencies?
- Search for old version shims we can remove
-1
View File
@@ -21,7 +21,6 @@ from ocrmypdf.exceptions import (
InputFileError,
MissingDependencyError,
OutputFileAccessError,
PdfMergeFailedError,
PriorOcrFoundError,
SubprocessOutputError,
TesseractConfigError,
-2
View File
@@ -71,6 +71,4 @@ def run(args=None):
if __name__ == '__main__':
if sys.platform == 'darwin' and sys.version_info < (3, 8):
set_start_method('spawn') # see python bpo-33725
sys.exit(run())
+5 -29
View File
@@ -14,6 +14,7 @@ from os import fspath
from pathlib import Path
from subprocess import PIPE, CalledProcessError
from packaging.version import Version
from PIL import Image, UnidentifiedImageError
from ocrmypdf.exceptions import SubprocessOutputError
@@ -47,21 +48,6 @@ 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:
match = re.search(r'error', stream, flags=re.IGNORECASE)
return bool(match)
@@ -201,20 +187,10 @@ def generate_pdfa(
]
strategy = 'LeaveColorUnchanged'
# Older versions of Ghostscript expect a leading slash in
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
# git commit fe1c025d.
gs_version = version()
strategy = ('/' + strategy) if gs_version < '9.19' else strategy
if gs_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')
elif gs_version == '9.56.0':
# 9.56.0 breaks our OCR...?
gs_version = Version(version())
if gs_version == Version('9.56.0'):
# 9.56.0 breaks our OCR, should be fixed in 9.56.1
# https://bugs.ghostscript.com/show_bug.cgi?id=705187
compression_args.append('-dNEWPDF=false')
# nb no need to specify ProcessColorModel when ColorConversionStrategy
+2 -11
View File
@@ -33,7 +33,7 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-system' content='tesseract 4.1.1' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
@@ -114,15 +114,6 @@ def version() -> str:
return get_version('tesseract', regex=r'tesseract\s(.+)')
def has_user_words() -> bool:
"""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 has_thresholding() -> bool:
"""Does Tesseract have -c thresholding method capability?"""
return version() >= '5.0'
@@ -244,7 +235,7 @@ def get_deskew(
def tesseract_log_output(stream: bytes) -> None:
tlog = TesseractLoggerAdapter(
log, extra=log.extra if hasattr(log, 'extra') else None
log, extra=log.extra if hasattr(log, 'extra') else None # type: ignore
)
if not stream:
+6 -4
View File
@@ -1,12 +1,10 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
"""Interface to unpaper executable"""
from __future__ import annotations
import logging
import os
import shlex
@@ -22,6 +20,10 @@ from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.subprocess import get_version, run
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
if sys.version_info >= (3, 10):
from tempfile import TemporaryDirectory
else:
+47 -42
View File
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-FileCopyrightText: 2018-2022 James R. Barlow
# SPDX-FileCopyrightText: 2019 Martin Wind
# SPDX-License-Identifier: MPL-2.0
"""OCRmyPDF page processing pipeline functions."""
@@ -13,7 +14,7 @@ from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path
from shutil import copyfileobj
from typing import Iterable
from typing import Any, Iterable, Sequence
import img2pdf
import pikepdf
@@ -35,7 +36,7 @@ from ocrmypdf.exceptions import (
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
from ocrmypdf.hocrtransform import HocrTransform
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
# Remove this workaround when we require Pillow >= 10
try:
@@ -49,7 +50,7 @@ log = logging.getLogger(__name__)
VECTOR_PAGE_DPI = 400
def triage_image_file(input_file, output_file, options):
def triage_image_file(input_file: Path, output_file: Path, options) -> None:
log.info("Input file is not a PDF, checking if it is an image...")
try:
im = Image.open(input_file)
@@ -113,7 +114,7 @@ def triage_image_file(input_file, output_file, options):
raise UnsupportedImageFormatError() from e
def _pdf_guess_version(input_file, search_window=1024):
def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
"""Try to find version signature at start of file.
Not robust enough to deal with appended files.
@@ -125,11 +126,13 @@ def _pdf_guess_version(input_file, search_window=1024):
signature = f.read(search_window)
m = re.search(br'%PDF-(\d\.\d)', signature)
if m:
return m.group(1)
return m.group(1).decode('ascii')
return ''
def triage(original_filename, input_file, output_file, options):
def triage(
original_filename: str, input_file: Path, output_file: Path, options
) -> Path:
try:
if _pdf_guess_version(input_file):
if options.image_dpi:
@@ -153,9 +156,9 @@ def get_pdfinfo(
input_file,
*,
executor: Executor,
detailed_analysis=False,
progbar=False,
max_workers=None,
detailed_analysis: bool = False,
progbar: bool = False,
max_workers: int | None = None,
check_pages=None,
) -> PdfInfo:
try:
@@ -173,7 +176,7 @@ def get_pdfinfo(
raise InputFileError() from e
def validate_pdfinfo_options(context: PdfContext):
def validate_pdfinfo_options(context: PdfContext) -> None:
pdfinfo = context.pdfinfo
options = context.options
@@ -214,11 +217,11 @@ def validate_pdfinfo_options(context: PdfContext):
context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options)
def _vector_page_dpi(pageinfo):
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0.0
def _vector_page_dpi(pageinfo: PageInfo) -> int:
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0
def get_page_dpi(pageinfo, options):
def get_page_dpi(pageinfo: PageInfo, options) -> Resolution:
"Get the DPI when nonsquare DPI is tolerable"
xres = max(
pageinfo.dpi.x or VECTOR_PAGE_DPI,
@@ -233,7 +236,7 @@ def get_page_dpi(pageinfo, options):
return Resolution(float(xres), float(yres))
def get_page_square_dpi(pageinfo, options) -> Resolution:
def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution:
"Get the DPI when we require xres == yres, scaled to physical units"
xres = pageinfo.dpi.x or 0.0
yres = pageinfo.dpi.y or 0.0
@@ -249,7 +252,7 @@ def get_page_square_dpi(pageinfo, options) -> Resolution:
return Resolution(units, units)
def get_canvas_square_dpi(pageinfo, options) -> Resolution:
def get_canvas_square_dpi(pageinfo: PageInfo, options) -> Resolution:
"""Get the DPI when we require xres == yres, in Postscript units"""
units = float(
max(
@@ -262,7 +265,7 @@ def get_canvas_square_dpi(pageinfo, options) -> Resolution:
return Resolution(units, units)
def is_ocr_required(page_context: PageContext):
def is_ocr_required(page_context: PageContext) -> bool:
pageinfo = page_context.pageinfo
options = page_context.options
@@ -337,7 +340,7 @@ def is_ocr_required(page_context: PageContext):
return ocr_required
def rasterize_preview(input_file: Path, page_context: PageContext):
def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
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)
@@ -354,7 +357,7 @@ def rasterize_preview(input_file: Path, page_context: PageContext):
return output_file
def describe_rotation(page_context: PageContext, orient_conf, correction: int):
def describe_rotation(page_context: PageContext, orient_conf, correction: int) -> str:
"""
Describe the page rotation we are going to perform.
"""
@@ -383,7 +386,7 @@ def describe_rotation(page_context: PageContext, orient_conf, correction: int):
return f"{facing}, confidence {orient_conf.confidence:.2f} - {action}"
def get_orientation_correction(preview: Path, page_context: PageContext):
def get_orientation_correction(preview: Path, page_context: PageContext) -> int:
"""Work out orientation correct for each page.
We ask Ghostscript to draw a preview page, which will rasterize with the
@@ -469,18 +472,17 @@ def rasterize(
return output_file
def preprocess_remove_background(input_file: Path, page_context: PageContext):
def preprocess_remove_background(input_file: Path, page_context: PageContext) -> Path:
if any(image.bpc > 1 for image in page_context.pageinfo.images):
raise NotImplementedError("--remove-background is temporarily not implemented")
# 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
log.info("background removal skipped on mono page")
return input_file
def preprocess_deskew(input_file: Path, page_context: PageContext):
def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
output_file = page_context.get_path('pp_deskew.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
@@ -500,7 +502,7 @@ def preprocess_deskew(input_file: Path, page_context: PageContext):
return output_file
def preprocess_clean(input_file: Path, page_context: PageContext):
def preprocess_clean(input_file: Path, page_context: PageContext) -> Path:
output_file = page_context.get_path('pp_clean.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
return unpaper.clean(
@@ -511,7 +513,7 @@ def preprocess_clean(input_file: Path, page_context: PageContext):
)
def create_ocr_image(image: Path, page_context: PageContext):
def create_ocr_image(image: Path, page_context: PageContext) -> Path:
"""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."""
@@ -559,7 +561,7 @@ def create_ocr_image(image: Path, page_context: PageContext):
return output_file
def ocr_engine_hocr(input_file: Path, page_context: PageContext):
def ocr_engine_hocr(input_file: Path, page_context: PageContext) -> tuple[Path, Path]:
hocr_out = page_context.get_path('ocr_hocr.hocr')
hocr_text_out = page_context.get_path('ocr_hocr.txt')
options = page_context.options
@@ -574,7 +576,7 @@ def ocr_engine_hocr(input_file: Path, page_context: PageContext):
return (hocr_out, hocr_text_out)
def should_visible_page_image_use_jpg(pageinfo):
def should_visible_page_image_use_jpg(pageinfo: PageInfo) -> bool:
# 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)
@@ -599,8 +601,8 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
def create_pdf_page_from_image(
image: Path, page_context: PageContext, orientation_correction
):
image: Path, page_context: PageContext, orientation_correction: int
) -> Path:
# 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
@@ -628,11 +630,10 @@ def create_pdf_page_from_image(
output_file = page_context.plugin_manager.hook.filter_pdf_page(
page=page_context, image_filename=image, output_pdf=output_file
)
return output_file
def render_hocr_page(hocr: Path, page_context: PageContext):
def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
options = page_context.options
output_file = page_context.get_path('ocr_hocr.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, options)
@@ -649,7 +650,9 @@ def render_hocr_page(hocr: Path, page_context: PageContext):
return output_file
def ocr_engine_textonly_pdf(input_image: Path, page_context: PageContext):
def ocr_engine_textonly_pdf(
input_image: Path, page_context: PageContext
) -> tuple[Path, Path]:
output_pdf = page_context.get_path('ocr_tess.pdf')
output_text = page_context.get_path('ocr_tess.txt')
options = page_context.options
@@ -695,13 +698,13 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
return pdfmark
def generate_postscript_stub(context: PdfContext):
def generate_postscript_stub(context: PdfContext) -> Path:
output_file = context.get_path('pdfa.ps')
generate_pdfa_ps(output_file)
return output_file
def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext):
def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -> Path:
options = context.options
input_pdfinfo = context.pdfinfo
fix_docinfo_file = context.get_path('fix_docinfo.pdf')
@@ -748,14 +751,14 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext):
return output_file
def should_linearize(working_file: Path, context: PdfContext):
def should_linearize(working_file: Path, context: PdfContext) -> bool:
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):
def get_pdf_save_settings(output_type: str) -> dict[str, Any]:
if output_type == 'pdfa-1':
# Trigger recompression to ensure object streams are removed, because
# Acrobat complains about them in PDF/A-1b validation.
@@ -773,7 +776,7 @@ def get_pdf_save_settings(output_type: str):
)
def metadata_fixup(working_file: Path, context: PdfContext):
def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
output_file = context.get_path('metafix.pdf')
options = context.options
@@ -829,7 +832,9 @@ def metadata_fixup(working_file: Path, context: PdfContext):
return output_file
def optimize_pdf(input_file: Path, context: PdfContext, executor: Executor):
def optimize_pdf(
input_file: Path, context: PdfContext, executor: Executor
) -> tuple[Path, Sequence[str]]:
output_file = context.get_path('optimize.pdf')
output_pdf, messages = context.plugin_manager.hook.optimize_pdf(
input_pdf=input_file,
@@ -865,7 +870,7 @@ def enumerate_compress_ranges(iterable):
yield (skipped_from, index), None
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext):
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Path:
output_file = context.get_path('sidecar.txt')
with open(output_file, 'w', encoding="utf-8") as stream:
for (from_, to_), txt_file in enumerate_compress_ranges(txt_files):
@@ -890,7 +895,7 @@ def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext):
return output_file
def copy_final(input_file, output_file, _context: PdfContext):
def copy_final(input_file, output_file, _context: PdfContext) -> None:
log.debug('%s -> %s', input_file, output_file)
with open(input_file, 'rb') as input_stream:
if output_file == '-':
+3 -2
View File
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-FileCopyrightText: 2019-2022 James R. Barlow
# SPDX-FileCopyrightText: 2019 Martin Wind
# SPDX-License-Identifier: MPL-2.0
"""Implements the concurrent and page synchronous parts of the pipeline."""
@@ -206,7 +207,7 @@ def exec_page_sync(page_context: PageContext) -> PageResult:
filtered_image = page_context.plugin_manager.hook.filter_page_image(
page=page_context, image_filename=visible_image_out
)
if filtered_image:
if filtered_image is not None: # None if no hook is present
visible_image_out = filtered_image
pdf_page_from_image_out = create_pdf_page_from_image(
visible_image_out, page_context, orientation_correction
+2 -2
View File
@@ -134,7 +134,7 @@ def check_options_preprocessing(options: Namespace) -> None:
package='unpaper',
version_checker=unpaper.version,
need_version='6.1',
required_for=['--clean, --clean-final'],
required_for="--clean, --clean-final", # Problem arguments
)
try:
if options.unpaper_args:
@@ -221,7 +221,7 @@ def check_options_metadata(options: Namespace) -> None:
def check_options_pillow(options: Namespace) -> None:
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
PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore
def _check_plugin_invariant_options(options: Namespace) -> None:
+1 -4
View File
@@ -8,10 +8,7 @@ OCRmyPDF uses setuptools_scm to derive version from git tags.
from __future__ import annotations
try:
from importlib.metadata import version as _package_version
except ImportError:
from importlib_metadata import version as _package_version # type: ignore
from importlib.metadata import version as _package_version
PROGRAM_NAME = 'ocrmypdf'
+4 -12
View File
@@ -15,6 +15,9 @@ from pathlib import Path
from typing import AnyStr, BinaryIO, Iterable, Union
from warnings import warn
import coloredlogs
from humanfriendly.terminal import enable_ansi_support
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
from ocrmypdf._plugin_manager import get_plugin_manager
from ocrmypdf._sync import run_pipeline
@@ -22,15 +25,6 @@ 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 # pylint: disable=invalid-name
if coloredlogs:
from humanfriendly.terminal import enable_ansi_support
StrPath = Union[Path, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
@@ -121,7 +115,7 @@ def configure_logging(
use_colors = progress_bar_friendly
formatter = None
if coloredlogs and use_colors:
if use_colors:
use_colors = enable_ansi_support()
if use_colors:
use_colors = coloredlogs.terminal_supports_colors()
@@ -284,8 +278,6 @@ def ocr( # pylint: disable=unused-argument
``"-"``, 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
+2 -2
View File
@@ -1,9 +1,9 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
"""OCRmyPDF's multiprocessing/multithreading abstraction layer."""
from __future__ import annotations
import logging
import logging.handlers
import multiprocessing
+2 -20
View File
@@ -21,37 +21,19 @@ def check_options(options):
program='gs',
package='ghostscript',
version_checker=ghostscript.version,
need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports
need_version='9.50', # Ubuntu 20.04's version
)
gs_version = ghostscript.version()
if gs_version in ('9.24', '9.51'):
if gs_version in ('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(
@@ -43,7 +43,7 @@ def add_options(parser):
metavar='MODE',
choices=range(0, 4),
help=(
"Set Tesseract 4.0+ OCR engine mode: "
"Set Tesseract 4+ OCR engine mode: "
"0 - original Tesseract only; "
"1 - neural nets LSTM only; "
"2 - Tesseract + LSTM; "
@@ -93,7 +93,7 @@ def check_options(options):
program='tesseract',
package={'linux': 'tesseract-ocr'},
version_checker=tesseract.version,
need_version='4.0.0-beta.1', # using backport for Travis CI
need_version='4.1.1', # Ubuntu 20.04 version
version_parser=tesseract.TesseractVersion,
)
@@ -101,11 +101,6 @@ def check_options(options):
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 (which you have installed) ignores --user-words and "
"--user-patterns, so these arguments have no effect."
)
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning(
"The installed version of Tesseract does not support changes to its "
-20
View File
@@ -47,26 +47,6 @@ class BadArgsError(ExitCodeException):
exit_code = ExitCode.bad_args
class PdfMergeFailedError(ExitCodeException): # deprecated
"""An intermediate PDF can't be merged.
No longer in use.
"""
exit_code = ExitCode.input_file
message = dedent(
'''\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is malformed and
ocrmypdf cannot correct the problem on its own.
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
'''
)
class MissingDependencyError(ExitCodeException):
"""A third-party dependency is missing."""
+25 -27
View File
@@ -10,14 +10,12 @@ import multiprocessing
import os
import shutil
import warnings
from collections import namedtuple
from collections.abc import Iterable
from contextlib import suppress
from functools import wraps
from io import StringIO
from math import isclose, isfinite
from pathlib import Path
from typing import Any, Sequence
from typing import Any, Generic, Sequence, SupportsFloat, SupportsRound, TypeVar
import img2pdf
import pikepdf
@@ -35,23 +33,33 @@ else:
)
class Resolution(namedtuple('Resolution', ('x', 'y'))):
T = TypeVar('T', bound=SupportsRound[Any])
class Resolution(Generic[T]):
"""The number of pixels per inch in each 2D direction.
Resolution objects are considered "equal" for == purposes if they are
equal to a reasonable tolerance.
"""
__slots__ = ()
x: T
y: T
__slots__ = ('x', 'y')
def __init__(self, x: T, y: T):
self.x = x
self.y = y
# rel_tol after converting from dpi to pixels per meter and saving
# as integer with rounding, as many file formats
CONVERSION_ERROR = 0.002
def round(self, ndigits: int):
def round(self, ndigits: int) -> Resolution:
return Resolution(round(self.x, ndigits), round(self.y, ndigits))
def to_int(self):
def to_int(self) -> Resolution[int]:
return Resolution(int(round(self.x)), int(round(self.y)))
@classmethod
@@ -64,9 +72,13 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
@property
def is_finite(self) -> bool:
return isfinite(self.x) and isfinite(self.y)
if isinstance(self.x, SupportsFloat) and isinstance(self.y, SupportsFloat):
return isfinite(self.x) and isfinite(self.y)
return True
def take_max(self, vals, yvals=None):
def take_max(
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
) -> Resolution:
if yvals is not None:
return Resolution(max(self.x, *vals), max(self.y, *yvals))
max_x, max_y = self.x, self.y
@@ -75,9 +87,12 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
max_y = max(y, max_y)
return Resolution(max_x, max_y)
def flip_axis(self):
def flip_axis(self) -> Resolution[T]:
return Resolution(self.y, self.x)
def __getitem__(self, idx: int | slice) -> T:
return (self.x, self.y)[idx]
def __str__(self):
return f"{self.x:f}x{self.y:f}"
@@ -275,20 +290,3 @@ def pikepdf_enable_mmap():
# Fix is not in pybind11 2.6.0
# log.debug("pikepdf mmap disabled")
return
def deprecated(func):
"""Warn that function is deprecated."""
@wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(
f"Call to deprecated function {func.__name__}.",
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)
return new_func
+2 -1
View File
@@ -14,10 +14,11 @@ import re
import warnings
from math import atan, cos, sin
from pathlib import Path
from typing import Any, NamedTuple, Optional, Tuple, Union
from typing import Any, NamedTuple
from xml.etree import ElementTree
with warnings.catch_warnings():
# reportlab uses deprecated load_module
warnings.filterwarnings(
'ignore', category=DeprecationWarning, message=r".*load_module.*"
)
+1 -1
View File
@@ -6,4 +6,4 @@
from __future__ import annotations
from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PdfInfo
from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PageInfo, PdfInfo
+4 -15
View File
@@ -17,16 +17,7 @@ from functools import partial
from math import hypot, inf, isclose
from os import PathLike
from pathlib import Path
from typing import (
Container,
Iterable,
Iterator,
Mapping,
NamedTuple,
Optional,
Sequence,
Tuple,
)
from typing import Container, Iterable, Iterator, Mapping, NamedTuple, Sequence, Tuple
from warnings import warn
from pikepdf import (
@@ -486,14 +477,12 @@ def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
resources = container['/Resources']
if '/XObject' not in resources:
return
xobjs = resources['/XObject'].as_dict()
for xobj in xobjs:
candidate: Object = xobjs[xobj]
if '/Subtype' not in candidate:
for key, candidate in resources['/XObject'].items():
if candidate is None or '/Subtype' not in candidate:
continue
if candidate['/Subtype'] == '/Image':
pdfimage = candidate
yield (pdfimage, xobj)
yield (pdfimage, key)
def _find_regular_images(
+21 -23
View File
@@ -1,5 +1,7 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Detailed text position and layout analysis, building on pdfminer.six."""
from __future__ import annotations
import re
@@ -23,14 +25,14 @@ from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
STRIP_NAME = re.compile(r'[0-9]+')
original_PDFSimpleFont_init = PDFSimpleFont.__init__
original_pdfsimplefont_init = PDFSimpleFont.__init__
def PDFSimpleFont__init__(self, descriptor, widths, spec):
def pdfsimplefont__init__(self, descriptor, widths, spec):
# Font encoding is specified either by a name of
# built-in encoding or a dictionary that describes
# the differences.
original_PDFSimpleFont_init(self, descriptor, widths, spec)
original_pdfsimplefont_init(self, descriptor, widths, spec)
# pdfminer is incorrect. If there is no ToUnicode and no Encoding, do not
# assume Unicode conversion is possible. RM 9.10.2
if not self.unicode_map and 'Encoding' not in spec:
@@ -38,25 +40,25 @@ def PDFSimpleFont__init__(self, descriptor, widths, spec):
return
PDFSimpleFont.__init__ = PDFSimpleFont__init__
PDFSimpleFont.__init__ = pdfsimplefont__init__
#
# pdfminer patches when creator is PScript5.dll
#
def PDFType3Font__PScript5_get_height(self):
def pdftype3font__pscript5_get_height(self):
h = self.bbox[3] - self.bbox[1]
if h == 0:
h = self.ascent - self.descent
return h * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_descent(self):
def pdftype3font__pscript5_get_descent(self):
return self.descent * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_ascent(self):
def pdftype3font__pscript5_get_ascent(self):
return self.ascent * copysign(1.0, self.vscale)
@@ -132,14 +134,14 @@ class LTStateAwareChar(LTChar):
return self._text
def __repr__(self):
return '<{} {} matrix={} rendermode={!r} font={!r} adv={} text={!r}>'.format(
self.__class__.__name__,
bbox2str(self.bbox),
matrix2str(self.matrix),
self.rendermode,
self.fontname,
self.adv,
self.get_text(),
return (
f"<{self.__class__.__name__} "
f"{bbox2str(self.bbox)} "
f"matrix={matrix2str(self.matrix)} "
f"rendermode={self.rendermode!r} "
f"font={self.fontname!r} "
f"adv={self.adv} "
f"text={self.get_text()!r}>"
)
@@ -207,11 +209,7 @@ class TextPositionTracker(PDFLayoutAnalyzer):
def get_page_analysis(infile, pageno, pscript5_mode):
rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
if pdfminer.__version__ < '20200402':
# Workaround for https://github.com/pdfminer/pdfminer.six/issues/395
disable_boxes_flow = 2
else:
disable_boxes_flow = None
disable_boxes_flow = None
dev = TextPositionTracker(
rman,
laparams=LAParams(
@@ -225,9 +223,9 @@ def get_page_analysis(infile, pageno, pscript5_mode):
patcher = patch.multiple(
'pdfminer.pdffont.PDFType3Font',
spec=True,
get_ascent=PDFType3Font__PScript5_get_ascent,
get_descent=PDFType3Font__PScript5_get_descent,
get_height=PDFType3Font__PScript5_get_height,
get_ascent=pdftype3font__pscript5_get_ascent,
get_descent=pdftype3font__pscript5_get_descent,
get_height=pdftype3font__pscript5_get_height,
)
patcher.start()
-4
View File
@@ -278,10 +278,6 @@ def filter_page_image(page: PageContext, image_filename: Path) -> Path:
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.
+2 -7
View File
@@ -9,7 +9,6 @@ import os
import re
import sys
from contextlib import suppress
from functools import lru_cache
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
from subprocess import run as subprocess_run
@@ -135,7 +134,6 @@ def _fix_process_args(
return args, env, process_log, text
@lru_cache(maxsize=None)
def get_version(
program: str,
*,
@@ -316,7 +314,7 @@ def check_external_program(
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_checker: 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.
@@ -327,10 +325,7 @@ def check_external_program(
"""
try:
if callable(version_checker):
found_version = version_checker()
else: # deprecated
found_version = version_checker
found_version = version_checker()
except (CalledProcessError, FileNotFoundError) as e:
_error_missing_program(program, package, required_for, recommended)
if not recommended:
+36 -9
View File
@@ -6,12 +6,15 @@ from __future__ import annotations
import logging
import os
import re
import shutil
import sys
from itertools import chain
from pathlib import Path
from typing import Any, Callable, Iterable, Iterator, TypeVar
from packaging.version import InvalidVersion, Version
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
@@ -35,6 +38,7 @@ else:
log = logging.getLogger(__name__)
T = TypeVar('T')
Tkey = TypeVar('Tkey')
def ghostscript_version_key(s: str) -> tuple[int, int, int]:
@@ -99,6 +103,30 @@ def registry_path_tesseract(env=None) -> Iterator[Path]:
log.warning(e)
def _gs_version_in_path_key(path: Path) -> tuple[str, Version | None]:
"""Key function for comparing Ghostscript and Tesseract paths.
Ghostscript installs on Windows:
%PROGRAMFILES%/gs/gs9.56.1/bin -> ('gs', Version('9.56.1'))
%PROGRAMFILES%/gs/9.24/bin -> ('gs', Version('9.24'))
Tesseract looks like:
%PROGRAMFILES%/Tesseract-OCR -> ('Tesseract-OCR', None)
Thus ensuring the resulting tuple will order the alternatives correctly,
e.g. gs10.0 > gs9.99.
"""
match = re.search(r'gs[/\\]?([0-9.]+)[/\\]bin', str(path))
if match:
try:
version_str = match.group(1)
version = Version(version_str)
return 'gs', version
except InvalidVersion:
pass
return path.name, None
def program_files_paths(env=None) -> Iterator[Path]:
if not env:
env = os.environ
@@ -116,7 +144,7 @@ def program_files_paths(env=None) -> Iterator[Path]:
return iter(
sorted(
(p for p in path_walker()),
key=lambda p: (p.name, p.parent.name),
key=_gs_version_in_path_key,
reverse=True,
)
)
@@ -143,11 +171,6 @@ SHIMS = [
def fix_windows_args(program: str, 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'):
@@ -164,11 +187,11 @@ def fix_windows_args(program: str, args, env):
return args
def unique_everseen(iterable: Iterable[T], key: Callable[[T], T]) -> Iterator[T]:
def unique_everseen(iterable: Iterable[T], key: Callable[[T], Tkey]) -> Iterator[T]:
"List unique elements, preserving order."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen: set[T] = set()
seen: set[Tkey] = set()
seen_add = seen.add
for element in iterable:
k = key(element)
@@ -177,11 +200,15 @@ def unique_everseen(iterable: Iterable[T], key: Callable[[T], T]) -> Iterator[T]
yield element
def _casefold_path(path: Path) -> str:
return str.casefold(str(path))
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)))
str(p) for p in unique_everseen(shim_paths, key=_casefold_path)
)
+6 -1
View File
@@ -44,7 +44,7 @@ TESTS_ROOT = Path(__file__).parent.resolve()
PROJECT_ROOT = TESTS_ROOT
@pytest.fixture
@pytest.fixture(scope="session")
def resources() -> Path:
return Path(TESTS_ROOT) / 'resources'
@@ -78,6 +78,11 @@ def no_outpdf(tmp_path) -> Path:
return tmp_path / 'no_output.pdf'
@pytest.fixture(scope="session")
def multipage(resources):
return resources / 'multipage.pdf'
def check_ocrmypdf(input_file: Path, output_file: Path, *args) -> Path:
"""Run ocrmypdf and confirm that a valid plausible PDF was created."""
api_args = [str(input_file), str(output_file)] + [
+2 -2
View File
@@ -9,13 +9,13 @@ 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
ELISION_WARNING = """GPL Ghostscript 9.50: Setting Overprint Mode to 1
not permitted in PDF/A-2, overprint mode not set"""
def run_append_stderr(*args, **kwargs):
proc = run_polling_stderr(*args, **kwargs)
proc.stderr += '\n' + elision_warning + '\n'
proc.stderr += '\n' + ELISION_WARNING + '\n'
return proc
+10 -9
View File
@@ -21,7 +21,7 @@ were produced.
Certain operations are not cached and routed to Tesseract OCR directly.
Assumes Tesseract 4.0.0-alpha or higher.
Assumes Tesseract 4+.
"""
@@ -140,18 +140,19 @@ def cached_run(options, run_args, **run_kwargs):
tessfile = args.outputbase + '.' + configfile
shutil.copy(tessfile, str(cache_folder / configfile) + '.bin')
manifest = {}
manifest['tesseract_version'] = TesseractOcrEngine.version().replace('\n', ' ')
manifest['system'] = platform.system()
manifest['python'] = platform.python_version()
manifest['argv_slug'] = cache_folder.name
manifest['sourcefile'] = str(Path(source_file).relative_to(TESTS_ROOT))
def clean_sys_argv():
for arg in run_args[1:]:
yield re.sub(r'.*/ocrmypdf[.]io[.][^/]+[/](.*)', r'$TMPDIR/\1', arg)
manifest['args'] = list(clean_sys_argv())
manifest = {
'tesseract_version': TesseractOcrEngine.version().replace('\n', ' '),
'system': platform.system(),
'python': platform.python_version(),
'argv_slug': cache_folder.name,
'sourcefile': str(Path(source_file).relative_to(TESTS_ROOT)),
'args': list(clean_sys_argv()),
}
with (Path(CACHE_ROOT) / 'manifest.jsonl').open('a') as f:
json.dump(manifest, f)
f.write('\n')
+2 -2
View File
@@ -27,7 +27,7 @@ HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-system' content='tesseract 4.1.1' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
</head>
<body>
@@ -46,7 +46,7 @@ HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
class FixedRotateNoopOcrEngine(OcrEngine):
@staticmethod
def version():
return '4.0.0'
return '4.1.1'
@staticmethod
def creator_tag(options):
+2 -2
View File
@@ -25,7 +25,7 @@ HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-system' content='tesseract 4.1.1' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
</head>
<body>
@@ -44,7 +44,7 @@ HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
class NoopOcrEngine(OcrEngine):
@staticmethod
def version():
return '4.0.0'
return '4.1.1'
@staticmethod
def creator_tag(options):
+41 -106
View File
@@ -1,168 +1,105 @@
.. SPDX-FileCopyrightText: 2022 James R. Barlow
.. SPDX-License-Identifier: CC-BY-SA-4.0
These test files are used in OCRmyPDF's test suite. They do not necessarily produce OCR results
at all and are not meant as examples of OCR output. Some are even invalid PDFs that might
at all and are not necessarily meant as examples of OCR output. Some are even invalid PDFs that might
crash certain PDF viewers.
Files derived from free sources
===============================
These test resources come from free sources, under either public domain or Creative Commons licenses.
In some cases they were converted from one image format to another without other changes.
Some of these images were obtained from the public domain. Others are copyrighted and may have
licenses associated. Refer to ``.reuse/dep5`` file in OCRmyPDF's Git repository for information on
the copyright holder(s) and license(s) applicable to these resources.
.. list-table::
:widths: 20 50 30
:widths: 15 35 50
:header-rows: 1
* - File
- Source
- License
- Purpose
* - c02-22.pdf
- `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22
- Public Domain
- difficult OCR image (obscure fonts and illustrations)
* - congress.jpg
- `US Congressional Records`_
- Public Domain
- difficult OCR image (color background)
* - graph.pdf
- `Wikimedia: Pandas text analysis.png`_
- Public Domain
- `Wikimedia:Simple_line_graph_of_ACE_2012_results_by_candidate_sj01.png`_
- image with slanted text
* - lichtenstein.pdf
- `Wikimedia: JPEG2000 Lichtenstein`_
- Creative Commons BY-SA 3.0
- JPEG2000 image
* - linn.png, linn.pdf, linn.txt
- `Wikimedia: LinnSequencer`_
- Creative Commons BY-SA 3.0
- image with two columns
* - typewriter.png, 2400dpi.pdf
- `Wikimedia: Triumph typewrtier text Linzensoep`_
- Creative Commons BY-SA 2.5
- simple text
* - baiona.png
- `Wikimedia: Baionako udalerri mugakideak`_
- Creative Commons BY-SA 4.0
* - enron1.pdf
- EnronData.org
- Creative Commons BY 3.0
Files generated for this project
================================
The following test resources were crafted specifically for this project, and are
licensed under the specified license.
.. list-table::
:widths: 20 40 15 15 10
:header-rows: 1
* - File
- Purpose
- Contributor
- Copyright Holder
- License
- multilingual text and images
* - aspect.pdf
- synthetic
- test image with 200 x 100 DPI resolution
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - blank.pdf
- synthetic
- blank PDF generated by Adobe Illustrator CC 17, containing a lot of application-specific metadata/bloat
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - cmyk.pdf
- synthetic
- a CMYK image created in Photoshop
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - crom.png
- synthetic
- test for non-dictionary words
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - enormous.pdf
- synthetic
- very large PDF page
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - epson.pdf
- synthetic
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC-BY-SA)
- @lowesjam
- Wikipedia authors
- CC-BY-SA 3.0
* - formxobject.pdf
- synthetic
- hand-crafted PDF containing an image inside a Form XObject
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - francais.pdf
- synthetic
- a page containing French accents (diacritics)
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - hugemono.pdf
- synthetic
- large monochrome 35000x35000 image in JBIG2 encoding
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - invalid.pdf
- synthetic
- a PDF file header followed by EOF marker
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - kcs.pdf
- synthetic
- PDF file generated by Kodak Capture Desktop Software 1.2; has invalid table of contents
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - livecycle.pdf
- synthetic
- a minimal PDF that claims to use dynamic XFA forms
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - masks.pdf
- synthetic
- file containing explicit masks and a stencil mask drawn without a proper transformation matrix; printout of a German Wikipedia article (CC-BY-SA)
- @supergrobi
- Wikipedia authors
- CC-BY-SA 3.0
* - missing_docinfo.pdf
- synthetic
- PDF file with no /DocumentInfo section
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - overlay.pdf
- synthetic
- PDF file generated by PDFPen pro that triggered content stream parse errors
- @maxandersen
- @maxandersen
- CC-BY-SA 4.0
* - negzero.pdf
- synthetic
- copy of formxobject.pdf with token that qpdf doesn't like
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - no_contents.pdf
- synthetic
- synthetic PDF with a blank page that has no /Contents entry
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - truetype_font_nomapping.pdf
- synthetic
- example of a PDF with an embedded subsetted TrueType font with no Unicode mapping
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - trivial.pdf
- synthetic
- smallest possible valid PDF-1.3 with all required fields
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - type3_font_nomapping.pdf
- synthetic
- example of a PDF with an embedded subsetted TrueType font with no Unicode mapping
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - vector.pdf
- synthetic
- a PDF with vector art and text rendered as curves with no fonts
- @Catscratch
- @Catscratch
- CC-BY-SA 4.0
Assemblies
==========
@@ -174,14 +111,12 @@ These test resources are assemblies or derivatives from other previously mention
- baiona_alpha.png (from baiona.png, RGB+A version)
- cardinal.pdf (four cardinal directions, baked-in rotated copies of linn.png)
- ccitt.pdf (linn.png, converted to CCITT encoding)
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
- graph_ocred.pdf (from graph.pdf)
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
- jbig2.pdf (from linn.png)
- multipage.pdf (from several other files)
- palette.pdf (congress.jpg, converted to a 256-color palette)
- palette.pdf (from baiona_colormapped.png)
- poster.pdf (from linn.png)
- rotated_skew.pdf (a /Rotate'd and skewed document from linn.png)
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2, password is "password")
- skew.pdf (from linn.png, skew simulated by adjusting the transformation matrix)
- toc.pdf (from formxobject.pdf, trivial.pdf)
@@ -192,7 +127,7 @@ These test resources are assemblies or derivatives from other previously mention
.. _`US Congressional Records`: http://www.baxleystamps.com/litho/meiji/courts_1871.jpg
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
.. _`Wikimedia: Simple_line_graph_of_ACE_2012_results_by_candidate_sj01.png`: https://en.wikipedia.org/wiki/File:Simple_line_graph_of_ACE_2012_results_by_candidate_sj01.png
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Regular → Executable
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -14,9 +14,9 @@ from .conftest import run_ocrmypdf_api
@pytest.mark.skipif(True, reason="--use-threads is currently default")
@pytest.mark.skipif(os.name == 'nt', reason="Windows doesn't have SIGKILL")
def test_simulate_oom_killer(resources, no_outpdf):
def test_simulate_oom_killer(multipage, no_outpdf):
exitcode = run_ocrmypdf_api(
resources / 'multipage.pdf',
multipage,
no_outpdf,
'--force-ocr',
'--plugin',
+15 -11
View File
@@ -6,9 +6,11 @@ from __future__ import annotations
import logging
import multiprocessing
import os
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from packaging.version import Version
from ocrmypdf import helpers
@@ -52,15 +54,6 @@ def test_no_cpu_count(monkeypatch):
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")
@@ -100,6 +93,17 @@ class TestFileIsWritable:
assert not helpers.is_file_writable(pathmock)
@pytest.mark.skipif(os.name != 'nt', reason="Windows test")
def test_gs_install_locations():
# pylint: disable=import-outside-toplevel
from ocrmypdf.subprocess._windows import _gs_version_in_path_key
assert _gs_version_in_path_key(Path("C:\\Program Files\\gs\\gs9.52\\bin")) == (
'gs',
Version('9.52'),
)
@pytest.mark.skipif(os.name != 'nt', reason="Windows test")
def test_shim_paths(tmp_path):
# pylint: disable=import-outside-toplevel
@@ -109,7 +113,7 @@ def test_shim_paths(tmp_path):
progfiles.mkdir()
(progfiles / 'tesseract-ocr').mkdir()
(progfiles / 'gs' / '9.51' / 'bin').mkdir(parents=True)
(progfiles / 'gs' / '9.52' / 'bin').mkdir(parents=True)
(progfiles / 'gs' / 'gs9.52.3' / 'bin').mkdir(parents=True)
syspath = tmp_path / 'bin'
env = {'PROGRAMFILES': str(progfiles), 'PATH': str(syspath)}
@@ -117,7 +121,7 @@ def test_shim_paths(tmp_path):
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[-2].endswith(os.path.join('gs9.52.3', 'bin')), results
assert results[-1].endswith(os.path.join('gs', '9.51', 'bin')), results
+26 -25
View File
@@ -7,16 +7,15 @@ import os
import shutil
from math import isclose
from pathlib import Path
from subprocess import PIPE, run
from subprocess import 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._exec import tesseract
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
@@ -153,9 +152,9 @@ def test_skip_big(resources, outpdf):
@pytest.mark.parametrize('renderer', RENDERERS)
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
def test_maximum_options(renderer, output_type, resources, outpdf):
def test_maximum_options(renderer, output_type, multipage, outpdf):
check_ocrmypdf(
resources / 'multipage.pdf',
multipage,
outpdf,
'-d',
'-ci' if have_unpaper() else None,
@@ -179,7 +178,8 @@ def test_maximum_options(renderer, output_type, resources, outpdf):
@pytest.mark.skipif(
tesseract.version() >= '5', reason="tess 5 tries harder to find its files"
tesseract.TesseractVersion(tesseract.version()) >= tesseract.TesseractVersion('5'),
reason="tess 5 tries harder to find its files",
)
def test_tesseract_missing_tessdata(monkeypatch, resources, no_outpdf, tmpdir):
monkeypatch.setenv("TESSDATA_PREFIX", os.fspath(tmpdir))
@@ -292,12 +292,6 @@ def test_input_file_not_a_pdf(caplog, no_outpdf):
assert input_file in caplog.text
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, resources, outpdf):
check_ocrmypdf(
@@ -398,14 +392,27 @@ def test_tesseract_image_too_big(renderer, resources, outpdf):
)
def test_algo4(resources, outpdf):
p = run_ocrmypdf(
resources / 'encrypted_algo4.pdf',
@pytest.mark.parametrize('encryption_level', [2, 3, 4, 6])
def test_encrypted(resources, outpdf, encryption_level, caplog):
encryption = pikepdf.models.encryption.Encryption(
owner='ocrmypdf',
user='ocrmypdf',
R=encryption_level,
aes=(encryption_level >= 4),
metadata=(encryption_level == 6),
)
with pikepdf.open(resources / 'jbig2.pdf') as pdf:
pdf.save(outpdf, encryption=encryption)
exitcode = run_ocrmypdf_api(
outpdf,
outpdf,
'--plugin',
'tests/plugins/tesseract_noop.py',
)
assert p.returncode == ExitCode.encrypted_pdf
assert exitcode == ExitCode.encrypted_pdf
assert 'encryption must be removed' in caplog.text
def test_jbig2_passthrough(resources, outpdf):
@@ -539,7 +546,6 @@ def test_tesseract_config_invalid(renderer, resources, invalid_tess_config, outp
assert p.returncode == ExitCode.invalid_config
@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'
@@ -716,11 +722,9 @@ def test_compression_changed(ocrmypdf_exec, resources, image, compression, outpd
if compression == "jpeg":
assert pdfimage.enc == Encoding.jpeg
else:
if ghostscript.jpeg_passthrough_available():
# Ghostscript 9.23 adds JPEG passthrough, which allows a JPEG to be
# copied without transcoding - so report
if image.endswith('jpg'):
assert pdfimage.enc == Encoding.jpeg
if image.endswith('jpg'):
# Ghostscript JPEG passthrough - no issue
assert pdfimage.enc == Encoding.jpeg
else:
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
@@ -774,9 +778,6 @@ def test_sidecar_nonempty(resources, outpdf):
@pytest.mark.parametrize('pdfa_level', ['1', '2', '3'])
def test_pdfa_n(pdfa_level, resources, outpdf):
if pdfa_level == '3' and ghostscript.version() < '9.19':
pytest.xfail(reason='Ghostscript >= 9.19 required')
check_ocrmypdf(
resources / 'ccitt.pdf',
outpdf,
+6 -4
View File
@@ -28,9 +28,6 @@ except ImportError:
fitz = None
pytestmark = pytest.mark.filterwarnings('ignore:.*XMLParser.*:DeprecationWarning')
@pytest.mark.parametrize("output_type", ['pdfa', 'pdf'])
def test_preserve_docinfo(output_type, resources, outpdf):
pdf_before = pikepdf.open(resources / 'graph.pdf')
@@ -174,7 +171,12 @@ def test_creation_date_preserved(output_type, resources, infile, outpdf):
def libxmp_file_to_dict():
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
# libxmp imports distutils.Version, which is deprecated
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
message=r".*distutils Version classes are deprecated.*",
)
from libxmp.utils import (
file_to_dict, # pylint: disable=import-outside-toplevel
)
+11 -3
View File
@@ -27,10 +27,18 @@ needs_jbig2enc = pytest.mark.skipif(
)
# pylint:disable=redefined-outer-name
@pytest.fixture(scope="session")
def palette(resources):
return resources / 'palette.pdf'
@needs_pngquant
@pytest.mark.parametrize('pdf', ['multipage.pdf', 'palette.pdf'])
def test_basic(resources, pdf, outpdf):
infile = resources / pdf
@pytest.mark.parametrize('pdf', ['multipage', 'palette'])
def test_basic(multipage, palette, pdf, outpdf):
infile = multipage if pdf == 'multipage' else palette
opt.main(infile, outpdf, level=3)
assert 0.98 * Path(outpdf).stat().st_size <= Path(infile).stat().st_size
+2 -3
View File
@@ -48,10 +48,9 @@ def test_nonmonotonic_warning(caplog):
assert 'out of order' in caplog.text
def test_limited_pages(resources, outpdf):
multi = resources / 'multipage.pdf'
def test_limited_pages(multipage, outpdf):
ocrmypdf.ocr(
multi,
multipage,
outpdf,
pages='5-6',
optimize=0,
+3 -2
View File
@@ -276,7 +276,8 @@ def test_rasterize_rotates(resources, tmp_path):
rotation=90,
filter_vector=False,
)
assert Image.open(img).size == (123, 151), "Image not rotated"
with Image.open(img) as im:
assert im.size == (83, 200), "Image not rotated"
img = tmp_path / 'img180.png'
pm.hook.rasterize_pdf_page(
@@ -289,7 +290,7 @@ def test_rasterize_rotates(resources, tmp_path):
rotation=180,
filter_vector=False,
)
assert Image.open(img).size == (151, 123), "Image not rotated"
assert Image.open(img).size == (200, 83), "Image not rotated"
def test_simulated_scan(outdir):
-4
View File
@@ -51,10 +51,6 @@ def test_stdout(ocrmypdf_exec, resources, outpdf):
assert check_pdf(output_file)
@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("Coverage uses stdout")
+39 -56
View File
@@ -48,28 +48,22 @@ def test_hocr_notlatin_warning(caplog):
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'), patch(
'ocrmypdf._exec.tesseract.get_languages', return_value={'eng', 'chi_sim'}
):
vd.check_options(*make_opts_pm(language='chi_sim', output_type='pdfa'))
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'))
with patch('ocrmypdf._exec.ghostscript.version', return_value='9.24'):
with pytest.raises(MissingDependencyError):
vd.check_options(*make_opts_pm())
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)
vd.check_options(*make_opts_pm(pdf_renderer='sandwich', language='eng'))
def test_tesseract_not_installed(caplog):
with patch('ocrmypdf.subprocess.run') as not_found:
not_found.side_effect = FileNotFoundError('tesseract')
with pytest.raises(MissingDependencyError, match="Could not find program"):
vd.check_options(*make_opts_pm())
assert (
"'tesseract' could not be executed" in caplog.text
), "Error message not printed"
assert 'install' in caplog.text, "Install advice not printed"
not_found.assert_called()
def test_lossless_redo():
@@ -87,32 +81,12 @@ def test_mutex_options():
def test_optimizing(caplog):
opts = make_opts(optimize=0, jbig2_lossy=True, png_quality=18, jpeg_quality=10)
plugin_manager = get_plugin_manager(opts.plugins)
vd.check_options(opts, plugin_manager)
vd.check_options(
*make_opts_pm(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)
assert (
'Tesseract 4.0 (which you have installed) 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)
assert (
'Tesseract 4.0 (which you have installed) ignores --user-words'
not in caplog.text
)
def test_pillow_options():
vd.check_options_pillow(make_opts(max_image_mpixels=0))
@@ -124,6 +98,8 @@ def test_output_tty():
def test_report_file_size(tmp_path, caplog):
logging.getLogger('pikepdf._qpdf').setLevel(logging.CRITICAL) # Suppress logging
in_ = tmp_path / 'a.pdf'
out = tmp_path / 'b.pdf'
pdf = pikepdf.new()
@@ -163,10 +139,10 @@ def test_false_action_store_true():
@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)
opts, pm = make_opts_pm(
progress_bar=progress_bar, input_file=(resources / 'trivial.pdf')
)
vd.check_options(opts, pm)
pbar_disabled = None
@@ -223,37 +199,44 @@ def test_version_comparison():
program="tesseract",
package="tesseract",
version_checker=lambda: '4.0.0-beta.1',
need_version='4.0.0',
need_version='4.1.1',
version_parser=TesseractVersion,
)
vd.check_external_program(
program="tesseract",
package="tesseract",
version_checker=lambda: 'v5.0.0-alpha.20200201',
need_version='4.0.0',
need_version='4.1.1',
version_parser=TesseractVersion,
)
vd.check_external_program(
program="tesseract",
package="tesseract",
version_checker=lambda: '5.0.0-rc1.20211030',
need_version='4.0.0',
need_version='4.1.1',
version_parser=TesseractVersion,
)
vd.check_external_program(
program="tesseract",
package="tesseract",
version_checker=lambda: 'v4.0.0.20181030', # Some Windows builds use this format
need_version='4.0.0',
version_checker=lambda: 'v4.1.1.20181030', # Some Windows builds use this format
need_version='4.1.1',
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,
program="gs",
package="ghostscript",
version_checker=lambda: '10.0',
need_version='9.50',
)
with pytest.raises(MissingDependencyError):
vd.check_external_program(
program="tesseract",
package="tesseract",
version_checker=lambda: '4.1.1-rc2-25-g9707',
need_version='4.1.1',
version_parser=TesseractVersion,
)
with pytest.raises(MissingDependencyError):
vd.check_external_program(
program="dummy_fails",