Compare commits

...
21 Commits
Author SHA1 Message Date
James R. Barlow 9b4516af7a ci : add packages for PyPy build 2021-08-31 02:44:07 -07:00
James R. Barlow 1eb45de5c9 v12.4.0 release notes 2021-08-31 02:35:39 -07:00
James R. Barlow 390b9924f5 ci: Add PyPy to matrix 2021-08-31 02:34:12 -07:00
James R. Barlow c28858a099 leptonica: fix a PyPy-specific error
Error is:
TypeError: from_buffer() got a 'memoryview' object, which supports the buffer interface but cannot be rendered as a plain raw address on PyPy

PyPy is happy to access a bytes() copy of the memoryview.
2021-08-31 02:31:58 -07:00
James R. Barlow f00b3c00cd Merge remote-tracking branch 'origin/master' 2021-08-31 02:19:09 -07:00
James R. Barlow 4e4f0bfa1f graft: use faster unparse_content_stream if available 2021-08-31 02:16:25 -07:00
Elliott Sales de AndradeandGitHub 0a31acf888 Allow pluggy v1. (#822)
There are breaking changes, but I could not find any reference to them
in the code.
2021-08-31 02:15:43 -07:00
James R. Barlow b91096c615 hoctransform: fix deprecation warning 2021-08-28 02:11:33 -07:00
James R. Barlow 95d9e8d91a info: inconsistent types used in ContentsInfo.name_index
This broke PyPy but CPython is fine with it.
2021-08-28 00:18:14 -07:00
James R. Barlow cb6c1939e9 typing: fix runtime issues 2021-08-27 02:18:54 -07:00
James R. Barlow 3764ee872a typing: refactor namedtuples in info 2021-08-27 00:51:11 -07:00
James R. Barlow e402d5cb4b typing: fix path and numeric issues 2021-08-27 00:23:38 -07:00
James R. Barlow 53cd04799a typing: fix Pillow usage, leptonica 2021-08-27 00:06:38 -07:00
James R. Barlow f2545d4496 typing: remove deprecated abstractstaticmethod 2021-08-26 23:51:11 -07:00
James R. Barlow 9b81e76ed4 typing: fix issues; avoid some magic literals 2021-08-26 23:48:27 -07:00
James R. Barlow 0956fc81aa typing: improvements for concurrency files 2021-08-26 23:47:53 -07:00
James R. Barlow 72279e7759 typing: confirmed that _pages_from_ranges(not-str) is never used 2021-08-26 23:47:11 -07:00
James R. Barlow 6f9b948064 typing: fix some trivial issues 2021-08-26 23:46:44 -07:00
James R. Barlow 4eca0a165b pre-commit: pyupgrade modernizing 2021-08-26 18:04:38 -07:00
James R. Barlow 067e61e03a pre-commit: auto update 2021-08-26 18:00:37 -07:00
James R. Barlow 1b46481f7e pre-commit: add setup.cfg fmt 2021-08-26 17:59:40 -07:00
27 changed files with 261 additions and 164 deletions
+12
View File
@@ -29,6 +29,10 @@ jobs:
python: 3.9 python: 3.9
- os: ubuntu-latest - os: ubuntu-latest
python: 3.9 python: 3.9
- os: ubuntu-20.04
python: "pypy-3.6"
- os: ubuntu-latest
python: "pypy-3.7"
- os: ubuntu-latest - os: ubuntu-latest
python: 3.9 python: 3.9
tesseract5: true tesseract5: true
@@ -82,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
View File
@@ -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
+1 -1
View File
@@ -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',
+12
View File
@@ -12,6 +12,18 @@ 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 v12.3.3
======= =======
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -63,7 +63,7 @@ exclude_lines = [
[tool.isort] [tool.isort]
profile = "black" profile = "black"
known_first_party = "ocrmypdf" 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"] 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] [tool.pytest.ini_options]
minversion = "6.0" minversion = "6.0"
+59 -56
View File
@@ -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,69 +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 =
coverage[toml] >= 5
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
@@ -101,4 +104,4 @@ test = pytest
[check-manifest] [check-manifest]
ignore = ignore =
.github .github
+6 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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)
+5 -6
View File
@@ -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
+1 -1
View File
@@ -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)
+4 -8
View File
@@ -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
View File
@@ -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()
+10 -8
View File
@@ -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
View File
@@ -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
+8 -4
View File
@@ -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:
+2 -2
View File
@@ -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,
) )
+1 -1
View File
@@ -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)
+13 -9
View File
@@ -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")
+1
View File
@@ -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)
+61 -27
View File
@@ -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:
+1 -1
View File
@@ -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),
+14 -8
View File
@@ -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
View File
@@ -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
-4
View File
@@ -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(
+2 -4
View File
@@ -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))