Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4516af7a | ||
|
|
1eb45de5c9 | ||
|
|
390b9924f5 | ||
|
|
c28858a099 | ||
|
|
f00b3c00cd | ||
|
|
4e4f0bfa1f | ||
|
|
0a31acf888 | ||
|
|
b91096c615 | ||
|
|
95d9e8d91a | ||
|
|
cb6c1939e9 | ||
|
|
3764ee872a | ||
|
|
e402d5cb4b | ||
|
|
53cd04799a | ||
|
|
f2545d4496 | ||
|
|
9b81e76ed4 | ||
|
|
0956fc81aa | ||
|
|
72279e7759 | ||
|
|
6f9b948064 | ||
|
|
4eca0a165b | ||
|
|
067e61e03a | ||
|
|
1b46481f7e | ||
|
|
d8d9c41abb | ||
|
|
a8cad72f72 | ||
|
|
86c04305f4 | ||
|
|
0a110fac55 | ||
|
|
f8970ad862 | ||
|
|
fcfc78b7ee | ||
|
|
8bb244df24 |
@@ -18,8 +18,24 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-18.04] #, ubuntu-20.04]
|
include:
|
||||||
python: ["3.6"] #, "3.7", "3.8", "3.9"]
|
- os: ubuntu-18.04
|
||||||
|
python: 3.6
|
||||||
|
- os: ubuntu-18.04
|
||||||
|
python: 3.7
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
python: 3.8
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
python: 3.9
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.9
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
python: "pypy-3.6"
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: "pypy-3.7"
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.9
|
||||||
|
tesseract5: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
@@ -35,6 +51,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
|
- name: Install Tesseract 5
|
||||||
|
if: matrix.tesseract5
|
||||||
|
run: |
|
||||||
|
sudo add-apt-repository ppa:alex-p/tesseract-ocr-devel
|
||||||
|
|
||||||
- name: Install common packages
|
- name: Install common packages
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -65,6 +86,14 @@ jobs:
|
|||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
libexempi8
|
libexempi8
|
||||||
|
|
||||||
|
- name: Install Ubuntu packages for PyPy
|
||||||
|
if: startsWith(matrix.python, 'pypy')
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
pypy3-dev
|
||||||
|
|
||||||
- name: Install Python packages
|
- name: Install Python packages
|
||||||
run: |
|
run: |
|
||||||
python -m pip install .[test]
|
python -m pip install .[test]
|
||||||
|
|||||||
+21
-3
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
@@ -12,12 +12,30 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: seed-isort-config
|
- id: seed-isort-config
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
rev: v5.7.0 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
rev: v5.9.3 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 21.7b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python
|
language_version: python
|
||||||
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
||||||
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
|
rev: v1.17.0
|
||||||
|
hooks:
|
||||||
|
- id: setup-cfg-fmt
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.24.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: ["--py36-plus"]
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v0.910
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
additional_dependencies:
|
||||||
|
- types-toml
|
||||||
|
- types-setuptools
|
||||||
|
- types-requests
|
||||||
|
- types-Pillow
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ 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.
|
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.
|
||||||
|
|
||||||
## Documentation and support
|
## Documentation and support
|
||||||
|
|
||||||
Once OCRmyPDF is installed, the built-in help which explains the command syntax and options can be accessed via:
|
Once OCRmyPDF is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||||
@@ -115,7 +120,7 @@ In addition to the required Python version (3.6+), OCRmyPDF requires external pr
|
|||||||
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](https://heise.de/-2356670)
|
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](https://heise.de/-2356670)
|
||||||
- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html)
|
- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html)
|
||||||
- [Excellent Utilities: OCRmyPDF](https://www.linuxlinks.com/excellent-utilities-ocrmypdf-add-ocr-text-layer-scanned-pdfs/)
|
- [Excellent Utilities: OCRmyPDF](https://www.linuxlinks.com/excellent-utilities-ocrmypdf-add-ocr-text-layer-scanned-pdfs/)
|
||||||
- [LinuxUser Texterkennung mit OCRmyPDF und Scanbd automatisieren](https://www.linux-community.de/ausgaben/linuxuser/2021/06/texterkennung-mit-ocrmypdf-und-scanbd-automatisieren/)
|
- [LinuxUser Texterkennung mit OCRmyPDF und Scanbd automatisieren](https://www.linux-community.de/ausgaben/linuxuser/2021/06/texterkennung-mit-ocrmypdf-und-scanbd-automatisieren/)
|
||||||
|
|
||||||
## Business enquiries
|
## Business enquiries
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -275,7 +275,7 @@ htmlhelp_basename = 'ocrmypdfdoc'
|
|||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
latex_elements = {
|
latex_elements = { # type: ignore
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
#
|
#
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
|
|||||||
+3
-3
@@ -9,11 +9,11 @@ encoding was patented for a long time. All known JBIG2 US patents have
|
|||||||
expired as of 2017, but it is possible that unknown patents exist.
|
expired as of 2017, but it is possible that unknown patents exist.
|
||||||
|
|
||||||
JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly
|
JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly
|
||||||
create smaller PDFs. If JBIG2 encoding not available, lower quality
|
create smaller PDFs. If JBIG2 encoding is not available, lower quality
|
||||||
encodings will be used.
|
encodings will be used.
|
||||||
|
|
||||||
JBIG2 decoding is not patented and is performed automatically by most
|
JBIG2 decoding is not patented and is performed automatically by most
|
||||||
PDF viewers. It is widely supported has been part of the PDF
|
PDF viewers. It is widely supported and has been part of the PDF
|
||||||
specification since 2001.
|
specification since 2001.
|
||||||
|
|
||||||
On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by
|
On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by
|
||||||
@@ -37,7 +37,7 @@ Lossy mode JBIG2
|
|||||||
|
|
||||||
OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should
|
OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should
|
||||||
`review the technical concerns with JBIG2 in lossy
|
`review the technical concerns with JBIG2 in lossy
|
||||||
mode <https://abbyy.technology/en:kb:tip:jbig2_compression_and_ocr>`__
|
mode <https://en.wikipedia.org/wiki/JBIG2#Disadvantages>`__
|
||||||
and decide if this feature is acceptable for their use case.
|
and decide if this feature is acceptable for their use case.
|
||||||
|
|
||||||
JBIG2 lossy mode does achieve higher compression ratios than any other
|
JBIG2 lossy mode does achieve higher compression ratios than any other
|
||||||
|
|||||||
@@ -12,6 +12,25 @@ may be unreliable. Use the API to depend on precise behavior.
|
|||||||
The public API may be useful in scripts that launch OCRmyPDF processes or that
|
The public API may be useful in scripts that launch OCRmyPDF processes or that
|
||||||
wish to use some of its features for working with PDFs.
|
wish to use some of its features for working with PDFs.
|
||||||
|
|
||||||
|
v12.4.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- When grafting text layers, use pikepdf's ``unparse_content_stream`` if available.
|
||||||
|
- Confirmed support for pluggy 1.0. (Thanks @QuLogic.)
|
||||||
|
- Fixed some typing issues, improved pre-commit settings, and fixed issues
|
||||||
|
flagged by linters.
|
||||||
|
- PyPy 7.3.3 (=Python 3.6) is now supported. Note that PyPy does not necessarily
|
||||||
|
run faster, because the vast majority of OCRmyPDF's execution time is spent
|
||||||
|
running OCR or generally executing native code. However, PyPy may bring speed
|
||||||
|
improvements in some areas.
|
||||||
|
|
||||||
|
v12.3.3
|
||||||
|
=======
|
||||||
|
|
||||||
|
- watcher.py: fixed interpretation of boolean env vars (:issue:`821`).
|
||||||
|
- Adjust CI scripts to test Tesseract 5 betas.
|
||||||
|
- Document our support for the Tesseract 5 betas.
|
||||||
|
|
||||||
v12.3.2
|
v12.3.2
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -37,14 +37,19 @@ import ocrmypdf
|
|||||||
|
|
||||||
# pylint: disable=logging-format-interpolation
|
# pylint: disable=logging-format-interpolation
|
||||||
|
|
||||||
|
|
||||||
|
def getenv_bool(name: str, default: str = 'False'):
|
||||||
|
return os.getenv(name, default).lower() in ('true', 'yes', 'y', '1')
|
||||||
|
|
||||||
|
|
||||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||||
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', ''))
|
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
|
||||||
ON_SUCCESS_DELETE = bool(os.getenv('OCR_ON_SUCCESS_DELETE', ''))
|
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
|
||||||
DESKEW = bool(os.getenv('OCR_DESKEW', ''))
|
DESKEW = getenv_bool('OCR_DESKEW')
|
||||||
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
||||||
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
||||||
USE_POLLING = bool(os.getenv('OCR_USE_POLLING', ''))
|
USE_POLLING = getenv_bool('OCR_USE_POLLING')
|
||||||
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
|
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
|
||||||
PATTERNS = ['*.pdf', '*.PDF']
|
PATTERNS = ['*.pdf', '*.PDF']
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ app.secret_key = "secret"
|
|||||||
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
|
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
|
||||||
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
|
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
|
||||||
|
|
||||||
ALLOWED_EXTENSIONS = set(["pdf"])
|
ALLOWED_EXTENSIONS = {"pdf"}
|
||||||
|
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
|
|||||||
@@ -34,3 +34,41 @@ exclude = '''
|
|||||||
| src/ocrmypdf/lib/_leptonica.py
|
| src/ocrmypdf/lib/_leptonica.py
|
||||||
)/
|
)/
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
parallel = true
|
||||||
|
concurrency = ["multiprocessing"]
|
||||||
|
|
||||||
|
[tool.coverage.paths]
|
||||||
|
source = ["src/ocrmypdf"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
# Regexes for lines to exclude from consideration
|
||||||
|
exclude_lines = [
|
||||||
|
# Have to re-enable the standard pragma
|
||||||
|
"pragma: no cover",
|
||||||
|
|
||||||
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
|
||||||
|
# Don't complain if non-runnable code isn't run:
|
||||||
|
"if 0:",
|
||||||
|
"if False:",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"if TYPE_CHECKING:"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
known_first_party = "ocrmypdf"
|
||||||
|
known_third_party = ["PIL", "_cffi_backend", "cffi", "flask", "img2pdf", "ocrmypdf", "pdfminer", "pikepdf", "pkg_resources", "pluggy", "pytest", "reportlab", "setuptools", "sphinx_rtd_theme", "tqdm", "watchdog", "werkzeug"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
addopts = "-n auto"
|
||||||
|
markers = ["slow"]
|
||||||
|
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
||||||
@@ -2,23 +2,15 @@
|
|||||||
name = ocrmypdf
|
name = ocrmypdf
|
||||||
description = OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched
|
description = OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched
|
||||||
long_description = file: README.md
|
long_description = file: README.md
|
||||||
long_description_content_type = text/markdown; charset=UTF-8
|
long_description_content_type = text/markdown
|
||||||
url = https://github.com/jbarlow83/OCRmyPDF
|
url = https://github.com/jbarlow83/OCRmyPDF
|
||||||
author = James R. Barlow
|
author = James R. Barlow
|
||||||
author_email = james@purplerock.ca
|
author_email = james@purplerock.ca
|
||||||
|
license = MPL-2.0
|
||||||
|
license_file = LICENSE
|
||||||
license_files =
|
license_files =
|
||||||
LICENSE
|
LICENSE
|
||||||
keywords =
|
|
||||||
PDF
|
|
||||||
OCR
|
|
||||||
optical character recognition
|
|
||||||
PDF/A
|
|
||||||
scanning
|
|
||||||
classifiers =
|
classifiers =
|
||||||
Programming Language :: Python :: 3.6
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Development Status :: 5 - Production/Stable
|
Development Status :: 5 - Production/Stable
|
||||||
Environment :: Console
|
Environment :: Console
|
||||||
Intended Audience :: End Users/Desktop
|
Intended Audience :: End Users/Desktop
|
||||||
@@ -30,68 +22,80 @@ classifiers =
|
|||||||
Operating System :: POSIX
|
Operating System :: POSIX
|
||||||
Operating System :: POSIX :: BSD
|
Operating System :: POSIX :: BSD
|
||||||
Operating System :: POSIX :: Linux
|
Operating System :: POSIX :: Linux
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3 :: Only
|
||||||
|
Programming Language :: Python :: 3.6
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
Topic :: Scientific/Engineering :: Image Recognition
|
Topic :: Scientific/Engineering :: Image Recognition
|
||||||
Topic :: Text Processing :: Indexing
|
Topic :: Text Processing :: Indexing
|
||||||
Topic :: Text Processing :: Linguistic
|
Topic :: Text Processing :: Linguistic
|
||||||
|
keywords =
|
||||||
|
PDF
|
||||||
|
OCR
|
||||||
|
optical character recognition
|
||||||
|
PDF/A
|
||||||
|
scanning
|
||||||
project_urls =
|
project_urls =
|
||||||
Documentation = https://ocrmypdf.readthedocs.io/
|
Documentation = https://ocrmypdf.readthedocs.io/
|
||||||
Source = https://github.com/jbarlow83/ocrmypdf
|
Source = https://github.com/jbarlow83/ocrmypdf
|
||||||
Tracker = https://github.com/jbarlow83/ocrmypdf/issues
|
Tracker = https://github.com/jbarlow83/ocrmypdf/issues
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
zip_safe = False
|
|
||||||
packages = find:
|
packages = find:
|
||||||
|
install_requires =
|
||||||
|
Pillow>=8.2.0
|
||||||
|
cffi>=1.9.1 # must be a setup and install requirement
|
||||||
|
coloredlogs>=14.0 # strictly optional
|
||||||
|
img2pdf>=0.3.0,<0.5 # pure Python
|
||||||
|
pdfminer.six!=20200720,>=20191110,<=20201018
|
||||||
|
pikepdf>=2.10.0
|
||||||
|
pluggy>=0.13.0,<2
|
||||||
|
reportlab>=3.5.66
|
||||||
|
setuptools
|
||||||
|
tqdm>=4
|
||||||
|
python_requires = >=3.6
|
||||||
|
include_package_data = True
|
||||||
package_dir =
|
package_dir =
|
||||||
=src
|
=src
|
||||||
platforms = any
|
platforms = any
|
||||||
include_package_data=True
|
setup_requires =
|
||||||
install_requires =
|
cffi>=1.9.1 # to build the leptonica module
|
||||||
cffi >= 1.9.1 # must be a setup and install requirement
|
setuptools_scm
|
||||||
coloredlogs >= 14.0 # strictly optional
|
setuptools_scm_git_archive
|
||||||
img2pdf >= 0.3.0, < 0.5 # pure Python, so track HEAD closely
|
zip_safe = False
|
||||||
pdfminer.six >= 20191110, != 20200720, <= 20201018
|
|
||||||
pikepdf >= 2.10.0
|
[options.packages.find]
|
||||||
Pillow >= 8.2.0
|
where = src
|
||||||
pluggy >= 0.13.0, < 1.0
|
|
||||||
reportlab >= 3.5.66
|
[options.entry_points]
|
||||||
setuptools
|
console_scripts =
|
||||||
tqdm >= 4
|
ocrmypdf = ocrmypdf.__main__:run
|
||||||
python_requires = >= 3.6
|
|
||||||
setup_requires = # can be removed whenever we can drop pip 9 support
|
[options.extras_require]
|
||||||
cffi >= 1.9.1 # to build the leptonica module
|
docs =
|
||||||
setuptools_scm # so that version will work
|
sphinx
|
||||||
setuptools_scm_git_archive # enable version from github tarballs
|
sphinx-issues
|
||||||
|
sphinx-rtd-theme
|
||||||
|
extended_test =
|
||||||
|
PyMuPDF==1.13.4
|
||||||
|
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
|
||||||
|
watcher =
|
||||||
|
watchdog>=1.0.2,<3
|
||||||
|
webservice =
|
||||||
|
Flask>=1,<3
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
ocrmypdf =
|
ocrmypdf =
|
||||||
data/sRGB.icc
|
data/sRGB.icc
|
||||||
py.typed
|
py.typed
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
where = src
|
|
||||||
|
|
||||||
[options.extras_require]
|
|
||||||
test =
|
|
||||||
pytest >= 6.0.0
|
|
||||||
pytest-xdist >= 2.2.0
|
|
||||||
pytest-cov >= 2.11.1
|
|
||||||
python-xmp-toolkit == 2.0.1 # also requires apt-get install libexempi3
|
|
||||||
# or brew install exempi
|
|
||||||
docs =
|
|
||||||
sphinx
|
|
||||||
sphinx-rtd-theme
|
|
||||||
sphinx-issues
|
|
||||||
extended_test =
|
|
||||||
PyMuPDF == 1.13.4
|
|
||||||
watcher =
|
|
||||||
watchdog >= 1.0.2, < 3
|
|
||||||
webservice =
|
|
||||||
Flask >= 1, < 3
|
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
ocrmypdf = ocrmypdf.__main__:run
|
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
python-tag = py36
|
python-tag = py36
|
||||||
|
|
||||||
@@ -100,48 +104,4 @@ test = pytest
|
|||||||
|
|
||||||
[check-manifest]
|
[check-manifest]
|
||||||
ignore =
|
ignore =
|
||||||
.github
|
.github
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
norecursedirs = lib .pc .git output cache resources
|
|
||||||
testpaths = tests
|
|
||||||
filterwarnings =
|
|
||||||
ignore:.*XMLParser.*:DeprecationWarning
|
|
||||||
markers =
|
|
||||||
slow
|
|
||||||
addopts =
|
|
||||||
-n auto
|
|
||||||
|
|
||||||
[isort]
|
|
||||||
multi_line_output = 3
|
|
||||||
include_trailing_comma = True
|
|
||||||
force_grid_wrap = 0
|
|
||||||
use_parentheses = True
|
|
||||||
line_length = 88
|
|
||||||
known_first_party = ocrmypdf
|
|
||||||
known_third_party = PIL,_cffi_backend,cffi,flask,img2pdf,pdfminer,pikepdf,pkg_resources,pluggy,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
|
|
||||||
|
|
||||||
[coverage:paths]
|
|
||||||
source =
|
|
||||||
src/ocrmypdf
|
|
||||||
|
|
||||||
[coverage:run]
|
|
||||||
branch = true
|
|
||||||
parallel = true
|
|
||||||
concurrency = multiprocessing
|
|
||||||
|
|
||||||
[coverage:report]
|
|
||||||
# Regexes for lines to exclude from consideration
|
|
||||||
exclude_lines =
|
|
||||||
# Have to re-enable the standard pragma
|
|
||||||
pragma: no cover
|
|
||||||
|
|
||||||
# Don't complain if tests don't hit defensive assertion code:
|
|
||||||
raise AssertionError
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
# Don't complain if non-runnable code isn't run:
|
|
||||||
if 0:
|
|
||||||
if False:
|
|
||||||
if __name__ == .__main__.:
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from ocrmypdf.api import StrPath
|
||||||
from ocrmypdf.exceptions import (
|
from ocrmypdf.exceptions import (
|
||||||
MissingDependencyError,
|
MissingDependencyError,
|
||||||
SubprocessOutputError,
|
SubprocessOutputError,
|
||||||
@@ -117,7 +118,7 @@ def get_languages():
|
|||||||
if line.startswith('Error'):
|
if line.startswith('Error'):
|
||||||
raise MissingDependencyError(lang_error(output))
|
raise MissingDependencyError(lang_error(output))
|
||||||
_header, *rest = output.splitlines()
|
_header, *rest = output.splitlines()
|
||||||
return set(lang.strip() for lang in rest)
|
return {lang.strip() for lang in rest}
|
||||||
|
|
||||||
|
|
||||||
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||||
@@ -250,7 +251,8 @@ def generate_hocr(
|
|||||||
|
|
||||||
# Reminder: test suite tesseract test plugins will break after any changes
|
# Reminder: test suite tesseract test plugins will break after any changes
|
||||||
# to the number of order parameters here
|
# to the number of order parameters here
|
||||||
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
|
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'hocr', 'txt'])
|
||||||
|
args_tesseract.extend(tessconfig)
|
||||||
try:
|
try:
|
||||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||||
stdout = p.stdout
|
stdout = p.stdout
|
||||||
@@ -324,7 +326,8 @@ def generate_pdf(
|
|||||||
# Reminder: test suite tesseract test plugins might break after any changes
|
# Reminder: test suite tesseract test plugins might break after any changes
|
||||||
# to the number of order parameters here
|
# to the number of order parameters here
|
||||||
|
|
||||||
args_tesseract.extend([input_file, prefix, 'pdf', 'txt'] + tessconfig)
|
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'pdf', 'txt'])
|
||||||
|
args_tesseract.extend(tessconfig)
|
||||||
try:
|
try:
|
||||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||||
stdout = p.stdout
|
stdout = p.stdout
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]:
|
|||||||
im = im.convert(mode='1')
|
im = im.convert(mode='1')
|
||||||
else:
|
else:
|
||||||
im = im.convert(mode='RGB')
|
im = im.convert(mode='RGB')
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
"Could not convert image with type " + im.mode
|
"Could not convert image with type " + im.mode
|
||||||
) from e
|
) from e
|
||||||
|
|||||||
+16
-9
@@ -73,17 +73,24 @@ def strip_invisible_text(pdf, page):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return str(op).encode('ascii')
|
return str(op).encode('ascii')
|
||||||
|
|
||||||
lines = []
|
if hasattr(pikepdf, 'unparse_content_stream'):
|
||||||
|
content_stream = pikepdf.unparse_content_stream(stream)
|
||||||
|
else:
|
||||||
|
lines = []
|
||||||
|
|
||||||
for operands, operator in stream:
|
for operands, operator in stream:
|
||||||
if operator == pikepdf.Operator('INLINE IMAGE'):
|
if operator == pikepdf.Operator('INLINE IMAGE'):
|
||||||
iim = operands[0]
|
iim = operands[0]
|
||||||
line = iim.unparse()
|
line = iim.unparse()
|
||||||
else:
|
else:
|
||||||
line = b' '.join(convert(op) for op in operands) + b' ' + operator.unparse()
|
line = (
|
||||||
lines.append(line)
|
b' '.join(convert(op) for op in operands)
|
||||||
|
+ b' '
|
||||||
|
+ operator.unparse()
|
||||||
|
)
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
content_stream = b'\n'.join(lines)
|
content_stream = b'\n'.join(lines)
|
||||||
page.Contents = pikepdf.Stream(pdf, content_stream)
|
page.Contents = pikepdf.Stream(pdf, content_stream)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def triage_image_file(input_file, output_file, options):
|
|||||||
log.info("Input file is not a PDF, checking if it is an image...")
|
log.info("Input file is not a PDF, checking if it is an image...")
|
||||||
try:
|
try:
|
||||||
im = Image.open(input_file)
|
im = Image.open(input_file)
|
||||||
except EnvironmentError as e:
|
except OSError as e:
|
||||||
# Recover the original filename
|
# Recover the original filename
|
||||||
log.error(str(e).replace(str(input_file), str(options.input_file)))
|
log.error(str(e).replace(str(input_file), str(options.input_file)))
|
||||||
raise UnsupportedImageFormatError() from e
|
raise UnsupportedImageFormatError() from e
|
||||||
@@ -135,7 +135,7 @@ def triage(original_filename, input_file, output_file, options):
|
|||||||
# Origin file is a pdf create a symlink with pdf extension
|
# Origin file is a pdf create a symlink with pdf extension
|
||||||
safe_symlink(input_file, output_file)
|
safe_symlink(input_file, output_file)
|
||||||
return output_file
|
return output_file
|
||||||
except EnvironmentError as e:
|
except OSError as e:
|
||||||
log.debug(f"Temporary file was at: {input_file}")
|
log.debug(f"Temporary file was at: {input_file}")
|
||||||
msg = str(e).replace(str(input_file), original_filename)
|
msg = str(e).replace(str(input_file), original_filename)
|
||||||
raise InputFileError(msg) from e
|
raise InputFileError(msg) from e
|
||||||
@@ -521,13 +521,12 @@ def create_ocr_image(image: Path, page_context: PageContext):
|
|||||||
# be None)
|
# be None)
|
||||||
bbox = [float(v) for v in textarea]
|
bbox = [float(v) for v in textarea]
|
||||||
xyscale = tuple(float(coord) / 72.0 for coord in im.info['dpi'])
|
xyscale = tuple(float(coord) / 72.0 for coord in im.info['dpi'])
|
||||||
pixcoords = [
|
pixcoords = (
|
||||||
bbox[0] * xyscale[0],
|
bbox[0] * xyscale[0],
|
||||||
im.height - bbox[3] * xyscale[1],
|
im.height - bbox[3] * xyscale[1],
|
||||||
bbox[2] * xyscale[0],
|
bbox[2] * xyscale[0],
|
||||||
im.height - bbox[1] * xyscale[1],
|
im.height - bbox[1] * xyscale[1],
|
||||||
]
|
)
|
||||||
pixcoords = [int(round(c)) for c in pixcoords]
|
|
||||||
log.debug('blanking %r', pixcoords)
|
log.debug('blanking %r', pixcoords)
|
||||||
draw.rectangle(pixcoords, fill=white)
|
draw.rectangle(pixcoords, fill=white)
|
||||||
# draw.rectangle(pixcoords, outline=pink)
|
# draw.rectangle(pixcoords, outline=pink)
|
||||||
@@ -856,7 +855,7 @@ def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
|
|||||||
if frm != 1:
|
if frm != 1:
|
||||||
stream.write('\f') # Form feed between pages
|
stream.write('\f') # Form feed between pages
|
||||||
if txt_file:
|
if txt_file:
|
||||||
with open(txt_file, 'r', encoding="utf-8") as in_:
|
with open(txt_file, encoding="utf-8") as in_:
|
||||||
txt = in_.read()
|
txt = in_.read()
|
||||||
# Some OCR engines (e.g. Tesseract v4 alpha) add form feeds
|
# Some OCR engines (e.g. Tesseract v4 alpha) add form feeds
|
||||||
# between pages, and some do not. For consistency, we ignore
|
# between pages, and some do not. For consistency, we ignore
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ def run_pipeline(options, *, plugin_manager, api=False):
|
|||||||
try:
|
try:
|
||||||
debug_log_handler.close()
|
debug_log_handler.close()
|
||||||
log.removeHandler(debug_log_handler)
|
log.removeHandler(debug_log_handler)
|
||||||
except EnvironmentError as e:
|
except OSError as e:
|
||||||
print(e, file=sys.stderr)
|
print(e, file=sys.stderr)
|
||||||
cleanup_working_files(work_folder, options)
|
cleanup_working_files(work_folder, options)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import sys
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
from typing import List, Set, Tuple, Union
|
from typing import List, Set, Tuple
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import PIL
|
import PIL
|
||||||
@@ -142,8 +142,6 @@ def check_options_preprocessing(options):
|
|||||||
|
|
||||||
|
|
||||||
def _pages_from_ranges(ranges: str) -> Set[int]:
|
def _pages_from_ranges(ranges: str) -> Set[int]:
|
||||||
if is_iterable_notstr(ranges):
|
|
||||||
return set(ranges)
|
|
||||||
pages: List[int] = []
|
pages: List[int] = []
|
||||||
page_groups = ranges.replace(' ', '').split(',')
|
page_groups = ranges.replace(' ', '').split(',')
|
||||||
for g in page_groups:
|
for g in page_groups:
|
||||||
@@ -182,10 +180,8 @@ def _pages_from_ranges(ranges: str) -> Set[int]:
|
|||||||
|
|
||||||
def check_options_ocr_behavior(options):
|
def check_options_ocr_behavior(options):
|
||||||
exclusive_options = sum(
|
exclusive_options = sum(
|
||||||
[
|
(1 if opt else 0)
|
||||||
(1 if opt else 0)
|
for opt in (options.force_ocr, options.skip_text, options.redo_ocr)
|
||||||
for opt in (options.force_ocr, options.skip_text, options.redo_ocr)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
if exclusive_options >= 2:
|
if exclusive_options >= 2:
|
||||||
raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
|
raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
|
||||||
@@ -302,7 +298,7 @@ def check_closed_streams(options): # pragma: no cover
|
|||||||
if options.input_file == '-':
|
if options.input_file == '-':
|
||||||
log.error("Trying to read from stdin but stdin seems closed")
|
log.error("Trying to read from stdin but stdin seems closed")
|
||||||
return False
|
return False
|
||||||
sys.stdin = open(os.devnull, 'r')
|
sys.stdin = open(os.devnull)
|
||||||
|
|
||||||
if sys.stdout is None:
|
if sys.stdout is None:
|
||||||
if options.output_file == '-':
|
if options.output_file == '-':
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ except ModuleNotFoundError:
|
|||||||
coloredlogs = None
|
coloredlogs = None
|
||||||
|
|
||||||
|
|
||||||
StrPath = Union[os.PathLike, AnyStr]
|
StrPath = Union[Path, AnyStr]
|
||||||
PathOrIO = Union[BinaryIO, StrPath]
|
PathOrIO = Union[BinaryIO, StrPath]
|
||||||
|
|
||||||
_api_lock = threading.Lock()
|
_api_lock = threading.Lock()
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from multiprocessing import Pool as ProcessPool
|
from multiprocessing.pool import Pool, ThreadPool
|
||||||
from multiprocessing.pool import ThreadPool
|
from typing import Callable, Iterable, Optional, Tuple, Type, Union
|
||||||
from typing import Callable, Iterable, Union
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
@@ -31,7 +30,10 @@ from ocrmypdf._logging import TqdmConsole
|
|||||||
from ocrmypdf.exceptions import InputFileError
|
from ocrmypdf.exceptions import InputFileError
|
||||||
from ocrmypdf.helpers import remove_all_log_handlers
|
from ocrmypdf.helpers import remove_all_log_handlers
|
||||||
|
|
||||||
|
ProcessPool = Pool
|
||||||
Queue = Union[multiprocessing.Queue, queue.Queue]
|
Queue = Union[multiprocessing.Queue, queue.Queue]
|
||||||
|
UserInit = Callable[[], None]
|
||||||
|
WorkerInit = Callable[[Queue, UserInit, int], None]
|
||||||
|
|
||||||
|
|
||||||
def log_listener(q: Queue):
|
def log_listener(q: Queue):
|
||||||
@@ -62,7 +64,7 @@ def process_sigbus(*args):
|
|||||||
raise InputFileError("A worker process lost access to an input file")
|
raise InputFileError("A worker process lost access to an input file")
|
||||||
|
|
||||||
|
|
||||||
def process_init(q: Queue, user_init: Callable[[], None], loglevel):
|
def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||||
"""Initialize a process pool worker"""
|
"""Initialize a process pool worker"""
|
||||||
|
|
||||||
# Ignore SIGINT (our parent process will kill us gracefully)
|
# Ignore SIGINT (our parent process will kill us gracefully)
|
||||||
@@ -85,7 +87,7 @@ def process_init(q: Queue, user_init: Callable[[], None], loglevel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def thread_init(_queue: Queue, user_init: Callable[[], None], _loglevel):
|
def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||||
# As a thread, block SIGBUS so the main thread deals with it...
|
# As a thread, block SIGBUS so the main thread deals with it...
|
||||||
with suppress(AttributeError):
|
with suppress(AttributeError):
|
||||||
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS})
|
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS})
|
||||||
@@ -107,9 +109,9 @@ class StandardExecutor(Executor):
|
|||||||
task_finished: Callable,
|
task_finished: Callable,
|
||||||
):
|
):
|
||||||
if use_threads:
|
if use_threads:
|
||||||
log_queue = queue.Queue(-1)
|
log_queue: Queue = queue.Queue(-1)
|
||||||
pool_class = ThreadPool
|
pool_class: Type[Pool] = ThreadPool
|
||||||
initializer = thread_init
|
initializer: WorkerInit = thread_init
|
||||||
else:
|
else:
|
||||||
log_queue = multiprocessing.Queue(-1)
|
log_queue = multiprocessing.Queue(-1)
|
||||||
pool_class = ProcessPool
|
pool_class = ProcessPool
|
||||||
|
|||||||
+5
-3
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from typing import Optional, Type, TypeVar
|
from typing import Any, Callable, Optional, TypeVar
|
||||||
|
|
||||||
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||||
from ocrmypdf._version import __version__ as _VERSION
|
from ocrmypdf._version import __version__ as _VERSION
|
||||||
@@ -14,7 +14,9 @@ from ocrmypdf._version import __version__ as _VERSION
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
def numeric(basetype: Type[T], min_: Optional[T] = None, max_: Optional[T] = None):
|
def numeric(
|
||||||
|
basetype: Callable[[Any], T], min_: Optional[T] = None, max_: Optional[T] = None
|
||||||
|
):
|
||||||
"""Validator for numeric params"""
|
"""Validator for numeric params"""
|
||||||
min_ = basetype(min_) if min_ is not None else None
|
min_ = basetype(min_) if min_ is not None else None
|
||||||
max_ = basetype(max_) if max_ is not None else None
|
max_ = basetype(max_) if max_ is not None else None
|
||||||
@@ -22,7 +24,7 @@ def numeric(basetype: Type[T], min_: Optional[T] = None, max_: Optional[T] = Non
|
|||||||
def _numeric(string):
|
def _numeric(string):
|
||||||
value = basetype(string)
|
value = basetype(string)
|
||||||
if (min_ is not None and value < min_) or (max_ is not None and value > max_):
|
if (min_ is not None and value < min_) or (max_ is not None and value > max_):
|
||||||
msg = "%r not in valid range %r" % (string, (min_, max_))
|
msg = f"{string!r} not in valid range {(min_, max_)!r}"
|
||||||
raise argparse.ArgumentTypeError(msg)
|
raise argparse.ArgumentTypeError(msg)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from enum import Enum, auto
|
|||||||
from itertools import islice, repeat, takewhile, zip_longest
|
from itertools import islice, repeat, takewhile, zip_longest
|
||||||
from multiprocessing import Pipe, Process
|
from multiprocessing import Pipe, Process
|
||||||
from multiprocessing.connection import Connection, wait
|
from multiprocessing.connection import Connection, wait
|
||||||
from typing import Callable, Iterable, Iterator
|
from typing import Callable, Iterable, Iterator, List
|
||||||
|
|
||||||
from ocrmypdf import Executor, hookimpl
|
from ocrmypdf import Executor, hookimpl
|
||||||
from ocrmypdf._concurrent import NullProgressBar
|
from ocrmypdf._concurrent import NullProgressBar
|
||||||
@@ -60,7 +60,9 @@ def process_sigbus(*args):
|
|||||||
|
|
||||||
class ConnectionLogHandler(logging.handlers.QueueHandler):
|
class ConnectionLogHandler(logging.handlers.QueueHandler):
|
||||||
def __init__(self, conn: Connection) -> None:
|
def __init__(self, conn: Connection) -> None:
|
||||||
super().__init__(None)
|
# sets the parent's queue to None - parent only touches queue
|
||||||
|
# in enqueue() which we override
|
||||||
|
super().__init__(None) # type: ignore
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
|
||||||
def enqueue(self, record):
|
def enqueue(self, record):
|
||||||
@@ -126,8 +128,8 @@ class LambdaExecutor(Executor):
|
|||||||
if not grouped_args:
|
if not grouped_args:
|
||||||
return
|
return
|
||||||
|
|
||||||
processes = []
|
processes: List[Process] = []
|
||||||
connections = []
|
connections: List[Connection] = []
|
||||||
for chunk in grouped_args:
|
for chunk in grouped_args:
|
||||||
parent_conn, child_conn = Pipe()
|
parent_conn, child_conn = Pipe()
|
||||||
|
|
||||||
@@ -152,6 +154,8 @@ class LambdaExecutor(Executor):
|
|||||||
with self.pbar_class(**tqdm_kwargs) as pbar:
|
with self.pbar_class(**tqdm_kwargs) as pbar:
|
||||||
while connections:
|
while connections:
|
||||||
for r in wait(connections):
|
for r in wait(connections):
|
||||||
|
if not isinstance(r, Connection):
|
||||||
|
raise NotImplementedError("We only support Connection()")
|
||||||
try:
|
try:
|
||||||
msg_type, msg = r.recv()
|
msg_type, msg = r.recv()
|
||||||
except EOFError:
|
except EOFError:
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def is_file_writable(test_file: os.PathLike) -> bool:
|
|||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
p.unlink()
|
p.unlink()
|
||||||
return True
|
return True
|
||||||
except (EnvironmentError, RuntimeError) as e:
|
except (OSError, RuntimeError) as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
log.error(str(e))
|
log.error(str(e))
|
||||||
return False
|
return False
|
||||||
@@ -273,7 +273,7 @@ def deprecated(func):
|
|||||||
def new_func(*args, **kwargs):
|
def new_func(*args, **kwargs):
|
||||||
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Call to deprecated function {}.".format(func.__name__),
|
f"Call to deprecated function {func.__name__}.",
|
||||||
category=DeprecationWarning,
|
category=DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ class HocrTransform:
|
|||||||
interword_spaces: bool,
|
interword_spaces: bool,
|
||||||
show_bounding_boxes: bool,
|
show_bounding_boxes: bool,
|
||||||
):
|
):
|
||||||
if not line:
|
if line is not None:
|
||||||
return
|
return
|
||||||
pxl_line_coords = self.element_coordinates(line)
|
pxl_line_coords = self.element_coordinates(line)
|
||||||
line_box = self.pt_from_pixel(pxl_line_coords)
|
line_box = self.pt_from_pixel(pxl_line_coords)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
|
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||||
#
|
#
|
||||||
@@ -13,6 +12,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -23,6 +23,7 @@ from functools import lru_cache
|
|||||||
from io import BytesIO, UnsupportedOperation
|
from io import BytesIO, UnsupportedOperation
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
from typing import ContextManager, Type
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from ocrmypdf.exceptions import MissingDependencyError
|
from ocrmypdf.exceptions import MissingDependencyError
|
||||||
@@ -67,7 +68,7 @@ if os.name == 'nt':
|
|||||||
# Loading zlib from other places could cause a version mismatch
|
# Loading zlib from other places could cause a version mismatch
|
||||||
_zlib_path = os.path.join(os.path.dirname(_libpath), 'zlib1.dll')
|
_zlib_path = os.path.join(os.path.dirname(_libpath), 'zlib1.dll')
|
||||||
if not os.path.exists(_zlib_path):
|
if not os.path.exists(_zlib_path):
|
||||||
_zlib_path = find_library('zlib')
|
_zlib_path = find_library('zlib') or ''
|
||||||
try:
|
try:
|
||||||
zlib = ffi.dlopen(_zlib_path)
|
zlib = ffi.dlopen(_zlib_path)
|
||||||
except ffi.error as e:
|
except ffi.error as e:
|
||||||
@@ -86,7 +87,7 @@ except ffi.error as e:
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class _LeptonicaErrorTrap_Redirect:
|
class _LeptonicaErrorTrap_Redirect(ContextManager):
|
||||||
"""
|
"""
|
||||||
Context manager to trap errors reported by Leptonica < 1.79 or on Apple Silicon.
|
Context manager to trap errors reported by Leptonica < 1.79 or on Apple Silicon.
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ class _LeptonicaErrorTrap_Redirect:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.leptonica_lock.release()
|
self.leptonica_lock.release()
|
||||||
raise
|
raise
|
||||||
return self
|
return
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
# Restore old stderr
|
# Restore old stderr
|
||||||
@@ -172,7 +173,7 @@ tls = threading.local()
|
|||||||
tls.trap = None
|
tls.trap = None
|
||||||
|
|
||||||
|
|
||||||
class _LeptonicaErrorTrap_Queue:
|
class _LeptonicaErrorTrap_Queue(ContextManager):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.queue = deque()
|
self.queue = deque()
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ except (ffi.error, MemoryError):
|
|||||||
# Pre-1.79 Leptonica does not have leptSetStderrHandler
|
# Pre-1.79 Leptonica does not have leptSetStderrHandler
|
||||||
# And some platforms, notably Apple ARM 64, do not allow the write+execute
|
# And some platforms, notably Apple ARM 64, do not allow the write+execute
|
||||||
# memory needed to set up the callback function.
|
# memory needed to set up the callback function.
|
||||||
_LeptonicaErrorTrap = _LeptonicaErrorTrap_Redirect
|
_LeptonicaErrorTrap: Type[ContextManager] = _LeptonicaErrorTrap_Redirect
|
||||||
else:
|
else:
|
||||||
# 1.79 have this new symbol
|
# 1.79 have this new symbol
|
||||||
_LeptonicaErrorTrap = _LeptonicaErrorTrap_Queue
|
_LeptonicaErrorTrap = _LeptonicaErrorTrap_Queue
|
||||||
@@ -272,7 +273,7 @@ class LeptonicaObject:
|
|||||||
# Leptonica API uses double-pointers for its destroy APIs to prevent
|
# Leptonica API uses double-pointers for its destroy APIs to prevent
|
||||||
# dangling pointers. This means we need to put our single pointer,
|
# dangling pointers. This means we need to put our single pointer,
|
||||||
# cdata, in a temporary CDATA**.
|
# cdata, in a temporary CDATA**.
|
||||||
pp = ffi.new('{} **'.format(cls.LEPTONICA_TYPENAME), cdata)
|
pp = ffi.new(f'{cls.LEPTONICA_TYPENAME} **', cdata)
|
||||||
cls.cdata_destroy(pp)
|
cls.cdata_destroy(pp)
|
||||||
|
|
||||||
|
|
||||||
@@ -439,6 +440,9 @@ class Pix(LeptonicaObject):
|
|||||||
bio = BytesIO()
|
bio = BytesIO()
|
||||||
pillow_image.save(bio, format='png', compress_level=1)
|
pillow_image.save(bio, format='png', compress_level=1)
|
||||||
py_buffer = bio.getbuffer()
|
py_buffer = bio.getbuffer()
|
||||||
|
if platform.python_implementation() == 'PyPy':
|
||||||
|
# PyPy complains that it cannot do from_buffer(memoryview)
|
||||||
|
py_buffer = bytes(py_buffer)
|
||||||
c_buffer = ffi.from_buffer(py_buffer)
|
c_buffer = ffi.from_buffer(py_buffer)
|
||||||
with _LeptonicaErrorTrap():
|
with _LeptonicaErrorTrap():
|
||||||
pix = Pix(lept.pixReadMem(c_buffer, len(c_buffer)))
|
pix = Pix(lept.pixReadMem(c_buffer, len(c_buffer)))
|
||||||
@@ -844,7 +848,7 @@ class Box(LeptonicaObject):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self._cdata:
|
if self._cdata:
|
||||||
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
|
return '<leptonica.Box x={} y={} w={} h={}>'.format(
|
||||||
self.x, self.y, self.w, self.h
|
self.x, self.y, self.w, self.h
|
||||||
)
|
)
|
||||||
return '<leptonica.Box NULL>'
|
return '<leptonica.Box NULL>'
|
||||||
@@ -916,7 +920,7 @@ class Sel(LeptonicaObject):
|
|||||||
lines = [line.strip() for line in selstr.split('\n') if line.strip()]
|
lines = [line.strip() for line in selstr.split('\n') if line.strip()]
|
||||||
h = len(lines)
|
h = len(lines)
|
||||||
w = len(lines[0])
|
w = len(lines[0])
|
||||||
lengths = set(len(line) for line in lines)
|
lengths = {len(line) for line in lines}
|
||||||
if len(lengths) != 1:
|
if len(lengths) != 1:
|
||||||
raise ValueError("All lines in selstr must be same length")
|
raise ValueError("All lines in selstr must be same length")
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ def convert_to_jbig2(
|
|||||||
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
|
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
|
||||||
and needs no dictionary. Currently this must be lossless JBIG2.
|
and needs no dictionary. Currently this must be lossless JBIG2.
|
||||||
"""
|
"""
|
||||||
|
jbig2_globals_dict: Optional[Dictionary]
|
||||||
|
|
||||||
_produce_jbig2_images(jbig2_groups, root, options, executor)
|
_produce_jbig2_images(jbig2_groups, root, options, executor)
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,21 @@ from functools import partial
|
|||||||
from math import hypot, inf, isclose
|
from math import hypot, inf, isclose
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Container, Iterator, Optional, Tuple, Union
|
from typing import (
|
||||||
|
Container,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
NamedTuple,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
from pikepdf import Object, Pdf, PdfMatrix
|
from pikepdf import Name, Object, Pdf, PdfInlineImage, PdfMatrix
|
||||||
|
|
||||||
from ocrmypdf._concurrent import Executor, SerialExecutor
|
from ocrmypdf._concurrent import Executor, SerialExecutor
|
||||||
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
||||||
@@ -36,7 +46,7 @@ Encoding = Enum(
|
|||||||
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate runlength'
|
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate runlength'
|
||||||
)
|
)
|
||||||
|
|
||||||
FRIENDLY_COLORSPACE = {
|
FRIENDLY_COLORSPACE: Dict[str, Colorspace] = {
|
||||||
'/DeviceGray': Colorspace.gray,
|
'/DeviceGray': Colorspace.gray,
|
||||||
'/CalGray': Colorspace.gray,
|
'/CalGray': Colorspace.gray,
|
||||||
'/DeviceRGB': Colorspace.rgb,
|
'/DeviceRGB': Colorspace.rgb,
|
||||||
@@ -54,7 +64,7 @@ FRIENDLY_COLORSPACE = {
|
|||||||
'/I': Colorspace.index,
|
'/I': Colorspace.index,
|
||||||
}
|
}
|
||||||
|
|
||||||
FRIENDLY_ENCODING = {
|
FRIENDLY_ENCODING: Dict[str, Encoding] = {
|
||||||
'/CCITTFaxDecode': Encoding.ccitt,
|
'/CCITTFaxDecode': Encoding.ccitt,
|
||||||
'/DCTDecode': Encoding.jpeg,
|
'/DCTDecode': Encoding.jpeg,
|
||||||
'/JPXDecode': Encoding.jpeg2000,
|
'/JPXDecode': Encoding.jpeg2000,
|
||||||
@@ -68,7 +78,7 @@ FRIENDLY_ENCODING = {
|
|||||||
'/RL': Encoding.runlength,
|
'/RL': Encoding.runlength,
|
||||||
}
|
}
|
||||||
|
|
||||||
FRIENDLY_COMP = {
|
FRIENDLY_COMP: Dict[Colorspace, int] = {
|
||||||
Colorspace.gray: 1,
|
Colorspace.gray: 1,
|
||||||
Colorspace.rgb: 3,
|
Colorspace.rgb: 3,
|
||||||
Colorspace.cmyk: 4,
|
Colorspace.cmyk: 4,
|
||||||
@@ -86,16 +96,30 @@ def _is_unit_square(shorthand):
|
|||||||
return all(isclose(a, b, rel_tol=1e-3) for a, b in pairwise)
|
return all(isclose(a, b, rel_tol=1e-3) for a, b in pairwise)
|
||||||
|
|
||||||
|
|
||||||
XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_depth'])
|
class XobjectSettings(NamedTuple):
|
||||||
|
name: str
|
||||||
|
shorthand: Tuple[float, float, float, float, float, float]
|
||||||
|
stack_depth: int
|
||||||
|
|
||||||
InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth'])
|
|
||||||
|
|
||||||
ContentsInfo = namedtuple(
|
class InlineSettings(NamedTuple):
|
||||||
'ContentsInfo',
|
iimage: PdfInlineImage
|
||||||
['xobject_settings', 'inline_images', 'found_vector', 'found_text', 'name_index'],
|
shorthand: Tuple[float, float, float, float, float, float]
|
||||||
)
|
stack_depth: int
|
||||||
|
|
||||||
TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt'])
|
|
||||||
|
class ContentsInfo(NamedTuple):
|
||||||
|
xobject_settings: List[XobjectSettings]
|
||||||
|
inline_images: List[InlineSettings]
|
||||||
|
found_vector: bool
|
||||||
|
found_text: bool
|
||||||
|
name_index: Mapping[str, List[XobjectSettings]]
|
||||||
|
|
||||||
|
|
||||||
|
class TextboxInfo(NamedTuple):
|
||||||
|
bbox: Tuple[float, float, float, float]
|
||||||
|
is_visible: bool
|
||||||
|
is_corrupt: bool
|
||||||
|
|
||||||
|
|
||||||
class VectorMarker:
|
class VectorMarker:
|
||||||
@@ -146,8 +170,8 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
|||||||
|
|
||||||
stack = []
|
stack = []
|
||||||
ctm = PdfMatrix(initial_shorthand)
|
ctm = PdfMatrix(initial_shorthand)
|
||||||
xobject_settings = []
|
xobject_settings: List[XobjectSettings] = []
|
||||||
inline_images = []
|
inline_images: List[InlineSettings] = []
|
||||||
name_index = defaultdict(lambda: [])
|
name_index = defaultdict(lambda: [])
|
||||||
found_vector = False
|
found_vector = False
|
||||||
found_text = False
|
found_text = False
|
||||||
@@ -185,7 +209,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
|||||||
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
|
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
|
||||||
)
|
)
|
||||||
xobject_settings.append(settings)
|
xobject_settings.append(settings)
|
||||||
name_index[image_name].append(settings)
|
name_index[str(image_name)].append(settings)
|
||||||
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
|
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
|
||||||
iimage = operands[0]
|
iimage = operands[0]
|
||||||
inline = InlineSettings(
|
inline = InlineSettings(
|
||||||
@@ -271,6 +295,9 @@ def _get_dpi(ctm_shorthand, image_size) -> Resolution:
|
|||||||
class ImageInfo:
|
class ImageInfo:
|
||||||
DPI_PREC = Decimal('1.000')
|
DPI_PREC = Decimal('1.000')
|
||||||
|
|
||||||
|
_comp: Optional[int]
|
||||||
|
_name: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -303,14 +330,14 @@ class ImageInfo:
|
|||||||
|
|
||||||
self._bpc = int(pim.bits_per_component)
|
self._bpc = int(pim.bits_per_component)
|
||||||
try:
|
try:
|
||||||
self._enc = FRIENDLY_ENCODING.get(pim.filters[0], 'image')
|
self._enc = FRIENDLY_ENCODING.get(pim.filters[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self._enc = '?'
|
self._enc = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._color = FRIENDLY_COLORSPACE.get(pim.colorspace, '?')
|
self._color = FRIENDLY_COLORSPACE.get(pim.colorspace)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
self._color = '?'
|
self._color = None
|
||||||
if self._enc == Encoding.jpeg2000:
|
if self._enc == Encoding.jpeg2000:
|
||||||
self._color = Colorspace.jpeg2000
|
self._color = Colorspace.jpeg2000
|
||||||
|
|
||||||
@@ -324,11 +351,14 @@ class ImageInfo:
|
|||||||
else:
|
else:
|
||||||
self._comp = 3
|
self._comp = 3
|
||||||
else:
|
else:
|
||||||
self._comp = FRIENDLY_COMP.get(self._color, '?')
|
if isinstance(self._color, Colorspace):
|
||||||
|
self._comp = FRIENDLY_COMP.get(self._color)
|
||||||
|
else:
|
||||||
|
self._comp = None
|
||||||
|
|
||||||
# Bit of a hack... infer grayscale if component count is uncertain
|
# Bit of a hack... infer grayscale if component count is uncertain
|
||||||
# but encoding only supports monochrome.
|
# but encoding only supports monochrome.
|
||||||
if self._comp == '?' and self._enc in (Encoding.ccitt, Encoding.jbig2):
|
if self._comp is None and self._enc in (Encoding.ccitt, Encoding.jbig2):
|
||||||
self._comp = FRIENDLY_COMP[Colorspace.gray]
|
self._comp = FRIENDLY_COMP[Colorspace.gray]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -353,15 +383,15 @@ class ImageInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self):
|
||||||
return self._color
|
return self._color if self._color is not None else '?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comp(self):
|
def comp(self):
|
||||||
return self._comp
|
return self._comp if self._comp is not None else '?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enc(self):
|
def enc(self):
|
||||||
return self._enc
|
return self._enc if self._enc is not None else 'image'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def renderable(self):
|
def renderable(self):
|
||||||
@@ -661,6 +691,10 @@ def _pdf_pageinfo_concurrent(
|
|||||||
|
|
||||||
|
|
||||||
class PageInfo:
|
class PageInfo:
|
||||||
|
_has_text: Optional[bool]
|
||||||
|
_has_vector: Optional[bool]
|
||||||
|
_images: List[ImageInfo]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pdf: Pdf,
|
pdf: Pdf,
|
||||||
@@ -732,7 +766,7 @@ class PageInfo:
|
|||||||
else:
|
else:
|
||||||
self._has_vector = None # i.e. "no information"
|
self._has_vector = None # i.e. "no information"
|
||||||
self._has_text = None
|
self._has_text = None
|
||||||
self._images = None
|
self._images = []
|
||||||
|
|
||||||
self._dpi = None
|
self._dpi = None
|
||||||
if self._images:
|
if self._images:
|
||||||
@@ -749,7 +783,7 @@ class PageInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_text(self) -> bool:
|
def has_text(self) -> bool:
|
||||||
return self._has_text
|
return bool(self._has_text)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_corrupt_text(self) -> bool:
|
def has_corrupt_text(self) -> bool:
|
||||||
@@ -759,7 +793,7 @@ class PageInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_vector(self) -> bool:
|
def has_vector(self) -> bool:
|
||||||
return self._has_vector
|
return bool(self._has_vector)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width_inches(self) -> Decimal:
|
def width_inches(self) -> Decimal:
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class LTStateAwareChar(LTChar):
|
|||||||
return self._text
|
return self._text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s %s matrix=%s rendermode=%r font=%r adv=%s text=%r>' % (
|
return '<{} {} matrix={} rendermode={!r} font={!r} adv={} text={!r}>'.format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
bbox2str(self.bbox),
|
bbox2str(self.bbox),
|
||||||
matrix2str(self.matrix),
|
matrix2str(self.matrix),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod, abstractstaticmethod
|
from abc import ABC, abstractmethod
|
||||||
from argparse import ArgumentParser, Namespace
|
from argparse import ArgumentParser, Namespace
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from logging import Handler
|
from logging import Handler
|
||||||
@@ -197,7 +197,7 @@ def rasterize_pdf_page(
|
|||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def filter_ocr_image(page: 'PageContext', image: 'Image') -> 'Image':
|
def filter_ocr_image(page: 'PageContext', image: 'Image.Image') -> 'Image.Image':
|
||||||
"""Called to filter the image before it is sent to OCR.
|
"""Called to filter the image before it is sent to OCR.
|
||||||
|
|
||||||
This is the image that OCR sees, not what the user sees when they view the
|
This is the image that OCR sees, not what the user sees when they view the
|
||||||
@@ -325,11 +325,13 @@ class OcrEngine(ABC):
|
|||||||
Tesseract OCR.
|
Tesseract OCR.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def version() -> str:
|
def version() -> str:
|
||||||
"""Returns the version of the OCR engine."""
|
"""Returns the version of the OCR engine."""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def creator_tag(options: Namespace) -> str:
|
def creator_tag(options: Namespace) -> str:
|
||||||
"""Returns the creator tag to identify this software's role in creating the PDF.
|
"""Returns the creator tag to identify this software's role in creating the PDF.
|
||||||
|
|
||||||
@@ -349,24 +351,28 @@ class OcrEngine(ABC):
|
|||||||
to the user, usually in an error message.
|
to the user, usually in an error message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def languages(options: Namespace) -> AbstractSet[str]:
|
def languages(options: Namespace) -> AbstractSet[str]:
|
||||||
"""Returns the set of all languages that are supported by the engine.
|
"""Returns the set of all languages that are supported by the engine.
|
||||||
|
|
||||||
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
|
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
|
||||||
can be any value understood by the OCR engine."""
|
can be any value understood by the OCR engine."""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def get_orientation(input_file: Path, options: Namespace) -> OrientationConfidence:
|
def get_orientation(input_file: Path, options: Namespace) -> OrientationConfidence:
|
||||||
"""Returns the orientation of the image."""
|
"""Returns the orientation of the image."""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def generate_hocr(
|
def generate_hocr(
|
||||||
input_file: Path, output_hocr: Path, output_text: Path, options: Namespace
|
input_file: Path, output_hocr: Path, output_text: Path, options: Namespace
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called to produce a hOCR file and sidecar text file."""
|
"""Called to produce a hOCR file and sidecar text file."""
|
||||||
|
|
||||||
@abstractstaticmethod
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
def generate_pdf(
|
def generate_pdf(
|
||||||
input_file: Path, output_pdf: Path, output_text: Path, options: Namespace
|
input_file: Path, output_pdf: Path, output_text: Path, options: Namespace
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
+2
-2
@@ -701,7 +701,7 @@ def test_sidecar_pagecount(resources, outpdf):
|
|||||||
pdfinfo = PdfInfo(resources / '3small.pdf')
|
pdfinfo = PdfInfo(resources / '3small.pdf')
|
||||||
num_pages = len(pdfinfo)
|
num_pages = len(pdfinfo)
|
||||||
|
|
||||||
with open(sidecar, 'r', encoding='utf-8') as f:
|
with open(sidecar, encoding='utf-8') as f:
|
||||||
ocr_text = f.read()
|
ocr_text = f.read()
|
||||||
|
|
||||||
# There should a formfeed between each pair of pages, so the count of
|
# There should a formfeed between each pair of pages, so the count of
|
||||||
@@ -722,7 +722,7 @@ def test_sidecar_nonempty(resources, outpdf):
|
|||||||
'tests/plugins/tesseract_cache.py',
|
'tests/plugins/tesseract_cache.py',
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(sidecar, 'r', encoding='utf-8') as f:
|
with open(sidecar, encoding='utf-8') as f:
|
||||||
ocr_text = f.read()
|
ocr_text = f.read()
|
||||||
assert 'the' in ocr_text
|
assert 'the' in ocr_text
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ def test_nonmonotonic_warning(caplog):
|
|||||||
assert 'out of order' in caplog.text
|
assert 'out of order' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_list_range():
|
|
||||||
assert _pages_from_ranges([0, 1, 2]) == {0, 1, 2}
|
|
||||||
|
|
||||||
|
|
||||||
def test_limited_pages(resources, outpdf):
|
def test_limited_pages(resources, outpdf):
|
||||||
multi = resources / 'multipage.pdf'
|
multi = resources / 'multipage.pdf'
|
||||||
ocrmypdf.ocr(
|
ocrmypdf.ocr(
|
||||||
|
|||||||
@@ -79,9 +79,7 @@ def test_dpi_needed(image, text, vector, result, rgb_image, outdir):
|
|||||||
# Input:
|
# Input:
|
||||||
('', '', '', '', ''),
|
('', '', '', '', ''),
|
||||||
# Output:
|
# Output:
|
||||||
(
|
(((1, 5), None),),
|
||||||
((1, 5), None),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'no_empty_values',
|
'no_empty_values',
|
||||||
@@ -147,4 +145,4 @@ def test_dpi_needed(image, text, vector, result, rgb_image, outdir):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_enumerate_compress_ranges(name, input, output):
|
def test_enumerate_compress_ranges(name, input, output):
|
||||||
assert output == tuple(_pipeline.enumerate_compress_ranges(input))
|
assert output == tuple(_pipeline.enumerate_compress_ranges(input))
|
||||||
|
|||||||
Reference in New Issue
Block a user