Change most tests to use ocrmypdf API instead of subprocess

The main benefit of this is code coverage gains can actually follow it.
Also removes most ugly os.environ hacks.
This commit is contained in:
James R. Barlow
2019-06-03 01:45:27 -07:00
parent 98a3fda1f5
commit eb5200d26a
18 changed files with 131 additions and 40 deletions
-2
View File
@@ -35,8 +35,6 @@ def run(args=None):
if not check_closed_streams(options):
return ExitCode.bad_args
if os.environ.get('PYTEST_CURRENT_TEST'):
os.environ['_OCRMYPDF_TEST_INFILE'] = options.input_file
if hasattr(os, 'nice'):
os.nice(5)
+3
View File
@@ -348,6 +348,7 @@ def get_orientation_correction(preview, page_context):
engine_mode=page_context.options.tesseract_oem,
timeout=page_context.options.tesseract_timeout,
log=page_context.log,
tesseract_env=page_context.options.tesseract_env,
)
direction = {0: '', 90: '', 180: '', 270: ''}
@@ -548,6 +549,7 @@ def ocr_tesseract_hocr(input_file, page_context):
pagesegmode=options.tesseract_pagesegmode,
user_words=options.user_words,
user_patterns=options.user_patterns,
tesseract_env=options.tesseract_env,
log=page_context.log,
)
return (hocr_out, hocr_text_out)
@@ -627,6 +629,7 @@ def ocr_tesseract_textonly_pdf(input_image, page_context):
pagesegmode=options.tesseract_pagesegmode,
user_words=options.user_words,
user_patterns=options.user_patterns,
tesseract_env=options.tesseract_env,
log=page_context.log,
)
return (output_pdf, output_text)
+23 -5
View File
@@ -16,6 +16,7 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import sys
from enum import IntEnum
from pathlib import Path
@@ -113,13 +114,16 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
def create_options(*, input_file, output_file, **kwargs):
cmdline = []
filters = []
deferred = []
for arg, val in kwargs.items():
if val is None:
continue
if arg.startswith('filter') and (callable(val) or isinstance(val, str)):
filters.append((arg, val))
deferred.append((arg, val))
continue
elif arg == 'tesseract_env':
deferred.append((arg, val))
continue
cmd_style_arg = arg.replace('_', '-')
cmdline.append(f"--{cmd_style_arg}")
@@ -132,15 +136,20 @@ def create_options(*, input_file, output_file, **kwargs):
elif isinstance(val, Path):
cmdline.append(str(val))
else:
raise TypeError(f"{val} ({type(val)})")
raise TypeError(f"{arg}: {val} ({type(val)})")
cmdline.append(str(input_file))
cmdline.append(str(output_file))
parser.api_mode = True
options = parser.parse_args(cmdline)
for keyword, function in filters:
setattr(options, keyword, function)
for keyword, val in deferred:
setattr(options, keyword, val)
# If we are running a Tesseract spoof, ensure it knows what the input file is
if os.environ.get('PYTEST_CURRENT_TEST') and options.tesseract_env:
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
return options
@@ -190,9 +199,18 @@ def ocrmypdf( # pylint: disable=unused-argument
keep_temporary_files=None,
progress_bar=None,
filter_ocr_image=None,
tesseract_env=None,
):
"""Run OCRmyPDF on one PDF or image.
For most arguments, see documentation for the equivalent command line parameter.
A few specific arguments are discussed here:
Args:
use_threads (bool): Use worker threads instead of processes. This reduces
performance but may make debugging easier since it is easier to set
breakpoints.
tesseract_env (dict): Override environment variables for Tesseract
Raises:
ocrmypdf.PdfMergeFailedError: If the input PDF is malformed, preventing merging
with the OCR layer.
+1
View File
@@ -482,3 +482,4 @@ debugging.add_argument(
action='store_true',
help="Keep temporary files (helpful for debugging)",
)
debugging.add_argument('--tesseract-env', type=str, help=argparse.SUPPRESS)
+2 -1
View File
@@ -28,7 +28,7 @@ from collections.abc import Mapping
log = logging.Logger(__name__)
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'):
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
"Get the version of the specified program"
args_prog = [program, version_arg]
try:
@@ -39,6 +39,7 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'):
stdout=PIPE,
stderr=STDOUT,
check=True,
env=env,
)
output = proc.stdout
except FileNotFoundError as e:
+54 -18
View File
@@ -60,18 +60,16 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
"""
@lru_cache(maxsize=1)
def version():
return get_version('tesseract', regex=r'tesseract\s(.+)')
def version(tesseract_env=None):
return get_version('tesseract', regex=r'tesseract\s(.+)', env=tesseract_env)
def v4():
def v4(tesseract_env=None):
"Is this Tesseract v4.0?"
return version() >= '4'
return version(tesseract_env) >= '4'
@lru_cache(maxsize=1)
def has_textonly_pdf():
def has_textonly_pdf(tesseract_env=None):
"""Does Tesseract have textonly_pdf capability?
Available in v4.00.00alpha since January 2017. Best to
@@ -80,7 +78,15 @@ def has_textonly_pdf():
args_tess = ['tesseract', '--print-parameters', 'pdf']
params = ''
try:
params = check_output(args_tess, universal_newlines=True, stderr=STDOUT)
proc = run(
args_tess,
check=True,
universal_newlines=True,
stdout=PIPE,
stderr=STDOUT,
env=tesseract_env,
)
params = proc.stdout
except CalledProcessError as e:
print("Could not --print-parameters from tesseract", file=sys.stderr)
raise MissingDependencyError from e
@@ -89,8 +95,7 @@ def has_textonly_pdf():
return False
@lru_cache(maxsize=1)
def languages():
def languages(tesseract_env=None):
def lang_error(output):
msg = dedent(
"""Tesseract failed to report available languages.
@@ -104,7 +109,12 @@ def languages():
args_tess = ['tesseract', '--list-langs']
try:
proc = run(
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True
args_tess,
universal_newlines=True,
stdout=PIPE,
stderr=STDOUT,
check=True,
env=tesseract_env,
)
output = proc.stdout
except CalledProcessError as e:
@@ -127,7 +137,7 @@ def tess_base_args(langs, engine_mode):
return args
def get_orientation(input_file, engine_mode, timeout: float, log):
def get_orientation(input_file, engine_mode, timeout: float, log, tesseract_env=None):
args_tesseract = tess_base_args(['osd'], engine_mode) + [
'--psm',
'0',
@@ -136,7 +146,15 @@ def get_orientation(input_file, engine_mode, timeout: float, log):
]
try:
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
p = run(
args_tesseract,
stdout=PIPE,
stderr=STDOUT,
timeout=timeout,
check=True,
env=tesseract_env,
)
stdout = p.stdout
except TimeoutExpired:
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
@@ -235,6 +253,7 @@ def generate_hocr(
pagesegmode: int,
user_words,
user_patterns,
tesseract_env,
log,
):
@@ -258,7 +277,15 @@ def generate_hocr(
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
p = run(
args_tesseract,
stdout=PIPE,
stderr=STDOUT,
timeout=timeout,
check=True,
env=tesseract_env,
)
stdout = p.stdout
except TimeoutExpired:
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
@@ -310,9 +337,10 @@ def generate_pdf(
pagesegmode: int,
user_words,
user_patterns,
tesseract_env,
log,
):
'''Use Tesseract to render a PDF.
"""Use Tesseract to render a PDF.
input_image -- image to analyze
skip_pdf -- if we time out, use this file as output
@@ -324,14 +352,14 @@ def generate_pdf(
tessconfig -- tesseract configuration
timeout -- timeout (seconds)
log -- logger object
'''
"""
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend(['--psm', str(pagesegmode)])
if text_only and has_textonly_pdf():
if text_only and has_textonly_pdf(tesseract_env):
args_tesseract.extend(['-c', 'textonly_pdf=1'])
if user_words:
@@ -348,7 +376,15 @@ def generate_pdf(
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
p = run(
args_tesseract,
stdout=PIPE,
stderr=STDOUT,
timeout=timeout,
check=True,
env=tesseract_env,
)
stdout = p.stdout
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -0,0 +1,13 @@
Portez ce vieux whisky au juge
blond qui fume sur son Ile
interieure, a cöte de l'alcöve
ovoide, oU les büches se
consument dans l'ätre, ce qui
lui permet de penser & la
caenogenese de |'etre dont il
est question dans la cause
ambigu& entendue a MoY, dans
un capharnaüm qui, pense-t-il,
diminue ca et la la qualite de son
ceuvre.
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -0,0 +1,13 @@
Portez ce vieux whisky au juge
blond qui fume sur son Ile
interieure, a cöte de l'alcöve
ovoide, oU les büches se
consument dans l'ätre, ce qui
lui permet de penser & la
caenogenese de |'etre dont il
est question dans la cause
ambigu& entendue a MoY, dans
un capharnaüm qui, pense-t-il,
diminue ca et la la qualite de son
ceuvre.
+2
View File
@@ -63,3 +63,5 @@
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.5.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.5.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.5.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cmyk.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
+14 -10
View File
@@ -21,6 +21,7 @@ import sys
from contextlib import contextmanager
from pathlib import Path
from subprocess import PIPE, run
from ocrmypdf import api, cli
import pytest
@@ -185,18 +186,21 @@ def no_outpdf(tmp_path):
def check_ocrmypdf(input_file, output_file, *args, env=None):
"""Run ocrmypdf and confirmed that a valid file was created"""
p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
# ensure py.test collects the output, use -s to view
print(err, file=sys.stderr)
assert p.returncode == 0
# p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
options = cli.parser.parse_args(
[str(input_file), str(output_file)] + [str(arg) for arg in args]
)
api.check_options(options)
if env:
options.tesseract_env = env
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
result = api.run_pipeline(options, api=True)
assert result == 0
assert os.path.exists(str(output_file)), "Output file not created"
assert os.stat(str(output_file)).st_size > 100, "PDF too small or empty"
assert out == "", (
"The following was written to stdout and should not have been: \n"
+ "<stdout>\n"
+ out
+ "\n</stdout>"
)
return output_file
-1
View File
@@ -26,7 +26,6 @@ from ocrmypdf.filters import invert, whiteout
from ocrmypdf._plugins import load_plugin
os_environ = pytest.helpers.os_environ
check_ocrmypdf = pytest.helpers.check_ocrmypdf
+4 -3
View File
@@ -40,7 +40,6 @@ from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
os_environ = pytest.helpers.os_environ
RENDERERS = ['hocr', 'sandwich']
@@ -614,8 +613,10 @@ def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
def test_masks(spoof_tesseract_noop, resources, outpdf):
with os_environ(spoof_tesseract_noop):
assert ocrmypdf(resources / 'masks.pdf', outpdf) == ExitCode.ok
assert (
ocrmypdf(resources / 'masks.pdf', outpdf, tesseract_env=spoof_tesseract_noop)
== ExitCode.ok
)
def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop, resources, outpdf):