Compare commits

...
32 Commits
Author SHA1 Message Date
James R. Barlow a6567f2ae4 v9.5.0 release notes revised 2020-01-18 01:48:33 -08:00
James R. Barlow e860c56b75 Fix regression: metadata updates not taking effect 2020-01-17 23:01:37 -08:00
James R. Barlow 2e15d52895 v9.5.0 release notes 2020-01-17 03:11:33 -08:00
James R. Barlow ce97af5a79 Add OCR quality measurement API 2020-01-17 03:10:27 -08:00
James R. Barlow 3831c4cd4d Refactor metadata_fixup 2020-01-14 01:10:15 -08:00
James R. Barlow 61a2674317 Skip test that needs chmod when on Windows 2020-01-06 02:36:04 -08:00
James R. Barlow 9ad8cbf1f6 Fix assert that depends on POSIX-y file handling 2020-01-06 02:02:05 -08:00
James R. Barlow 123fde174d Don't use debug.log in pytest
pytest does not reset the state of logging if we install a file handler,
which will cause FileNotFoundError after the temporary folder is removed.

Semi-related:
https://github.com/pytest-dev/pytest/issues/5502
2020-01-06 01:46:19 -08:00
James R. Barlow fd991a2380 Allow pdfminer.six 20200104 and update recommended versions 2020-01-05 21:37:28 -08:00
James R. Barlow 6f5d77d930 Also generate log file in temp folder on verbose mode 2020-01-05 21:33:32 -08:00
James R. Barlow 5169ac633b docs: mention pdfgrep too 2020-01-05 21:32:36 -08:00
James R. Barlow 5b6ab1e003 lept: improve lib not found error message
Closes #471
2020-01-05 01:05:53 -08:00
James R. Barlow 8f984bf958 docs: add note on limitations of sidecar file 2020-01-04 16:43:13 -08:00
James R. Barlow 9c5f0d0ec6 Eliminate last use of PyPDF2 from test suite 2020-01-04 16:32:01 -08:00
James R. Barlow 32041c43e1 tests: improve tesseract coverage 2020-01-04 02:35:14 -08:00
James R. Barlow 599028bebb tesseract: don't explicitly set lstm_use_matrix
Apparently tesseract does this own its own as needed.
2020-01-04 01:17:33 -08:00
James R. Barlow 6faa8f7221 logging: always log process arguments and stderr when at debug
Also remove ad-hoc logging of this information.
2020-01-01 16:48:48 -08:00
James R. Barlow a4dc5e365f logging: fix incorrect usage: logging.Logger() 2020-01-01 16:47:36 -08:00
James R. Barlow e2a563cc76 logging: create a debug log when -k parameter is issued 2020-01-01 16:47:15 -08:00
James R. Barlow 1037d73efb tests: use smaller files for ghostscript 2019-12-31 17:20:28 -08:00
James R. Barlow aeb7b142a9 tests: skip tests not compatible with coverage
For reasons not entirely clear, stdout will get some data injected when
pytest-cov is running. Our tests that
check for clean stdout need to ignore this.

We check for an environment variable that is defined only when coverage is
running.
2019-12-31 17:10:51 -08:00
James R. Barlow 422ea9777e Remove session scope from fixtures
pytest seems to prepare os.environ in complex ways, so we want to ensure
these fixtures are not reused.
2019-12-31 17:09:23 -08:00
James R. Barlow 2f1c743227 Rewrite main pool loop
pytest-cov documentation recommends using explicit
management of multiprocessing.Pool rather than the context manager.
This is supposed to work better for collecting coverage data, particularly
on Windows.
2019-12-31 16:23:41 -08:00
James R. Barlow 96ee21aee9 Try to set up subprocess coverage better 2019-12-31 15:39:45 -08:00
James R. Barlow 4b759af6ff tests: fix problems with ghostscript spoofers 2019-12-31 15:33:03 -08:00
James R. Barlow 25d2b0cda4 test: environment warnings/cleanup 2019-12-30 22:38:50 -08:00
James R. Barlow 16dd8b54a8 ghostscript: don't delete output_file that will never exist
We stream output now, so no point in deleting.
2019-12-30 22:38:38 -08:00
James R. Barlow c4dc5269d2 tests: remove some obscure things from coverage 2019-12-30 21:16:16 -08:00
James R. Barlow c36e9950ae tests: test TqdmConsole 2019-12-30 17:51:09 -08:00
James R. Barlow 0c0d53b10f tests: AcroForm test case did not work correctly; fixed 2019-12-30 17:50:32 -08:00
James R. Barlow 63de7e1677 Improve error message for unreadable input files 2019-12-30 16:14:52 -08:00
James R. Barlow b0e92760a2 tests: add coverage for helpers 2019-12-30 15:52:10 -08:00
33 changed files with 671 additions and 188 deletions
+3 -3
View File
@@ -1,5 +1,3 @@
# Coverage isn't really compatible with subprocesses so results are unreliable
[paths]
source =
src
@@ -8,9 +6,11 @@ source =
[run]
branch = true
parallel = true
concurrency =
thread
multiprocessing
source =
src/ocrmypdf
tests
omit =
tests/spoof/*
+12
View File
@@ -89,6 +89,18 @@ This produces a file named "output.pdf" and a companion text file named
ocrmypdf --sidecar output.txt input.pdf output.pdf
.. note::
The sidecar file contains the **OCR text** found by OCRmyPDF. If the document
contains pages that already have text, that text will not appear in the
sidecar. If the option ``--pages`` is used, only those pages on which OCR
was performed will be included in the sidecar. If certain pages were skipped
because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages
will not be in the sidecar.
To extract all text from a PDF, whether generated from OCR or otherwise,
use a program like Poppler's ``pdftotext`` or ``pdfgrep``.
OCR images, not PDFs
--------------------
+18
View File
@@ -13,6 +13,24 @@ Note that it is licensed under GPLv3, so scripts that
``import ocrmypdf`` and are released publicly should probably also be
licensed under GPLv3.
v9.5.0
======
- Added API functions to measure OCR quality.
- Modest improvements to handling PDFs with difficult/non compliant metadata.
v9.4.0
======
- Updated recommended dependency versions.
- Improvements to test coverage and changes to facilitate better measurement of
test coverage, such as when tests run in subprocesses.
- Improvements to error messages when Leptonica is not installed correctly.
- Fixed use of pytest "session scope" that may have caused some intermittent
CI failures.
- When the argument ``--keep-temporary-files`` or verbosity is set to ``-v1``,
a debug log file is generated in the working temporary folder.
v9.3.0
======
+4 -4
View File
@@ -3,8 +3,8 @@
# installation
cffi == 1.13.2
img2pdf == 0.3.3
pdfminer.six == 20191110
pikepdf == 1.8.1
Pillow >= 6.2.0
pdfminer.six == 20200104
pikepdf == 1.8.2
Pillow == 7.0.0
reportlab == 3.5.32
tqdm == 4.37.0
tqdm == 4.41.1
+2 -3
View File
@@ -1,8 +1,7 @@
pytest >= 5.0.0
pytest-helpers-namespace >= 2019.1.8
pytest-xdist >= 1.29.0 # For DumpError fix
pytest-cov >= 2.6.1
pytest-xdist >= 1.31.0
pytest-cov >= 2.8.0
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
# or brew install exempi
PyPDF2 >= 1.26.0
#PyMuPDF == 1.13.4 # optional
+1 -1
View File
@@ -23,7 +23,7 @@ force_grid_wrap=0
use_parentheses=True
line_length=88
known_first_party = ocrmypdf
known_third_party = PIL,PyPDF2,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
[metadata]
license_file = LICENSE
+3 -2
View File
@@ -21,11 +21,12 @@ from __future__ import print_function, unicode_literals
import sys
from setuptools import find_packages, setup
if sys.version_info < (3, 6):
print("Python 3.6 or newer is required", file=sys.stderr)
sys.exit(1)
from setuptools import setup, find_packages
# pylint: disable=w0613
@@ -97,7 +98,7 @@ setup(
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
'pdfminer.six >= 20181108, <= 20191110',
'pdfminer.six >= 20181108, <= 20200104',
'pikepdf >= 1.8.1, < 2',
'Pillow >= 6.2.0',
'reportlab >= 3.3.0', # oldest released version with sane image handling
+1
View File
@@ -23,6 +23,7 @@ from .exceptions import (
DpiError,
EncryptedPdfError,
ExitCode,
ExitCodeException,
InputFileError,
MissingDependencyError,
OutputFileAccessError,
+52 -44
View File
@@ -19,6 +19,7 @@ import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from shutil import copyfileobj
import img2pdf
@@ -123,7 +124,7 @@ def _pdf_guess_version(input_file, search_window=1024):
return ''
def triage(input_file, output_file, options, log):
def triage(original_filename, input_file, output_file, options, log):
try:
if _pdf_guess_version(input_file):
if options.image_dpi:
@@ -135,8 +136,9 @@ def triage(input_file, output_file, options, log):
safe_symlink(input_file, output_file)
return output_file
except EnvironmentError as e:
log.error(e)
raise InputFileError() from e
log.debug(f"Temporary file was at: {input_file}")
msg = str(e).replace(input_file, original_filename)
raise InputFileError(msg) from e
triage_image_file(input_file, output_file, options, log)
return output_file
@@ -181,7 +183,7 @@ def validate_pdfinfo_options(context):
)
raise InputFileError()
else:
log.warn(
log.warning(
"This PDF has a fillable form. "
"Chances are it is a pure digital "
"document that does not need OCR."
@@ -725,47 +727,53 @@ def should_linearize(working_file, context):
def metadata_fixup(working_file, context):
output_file = context.get_path('metafix.pdf')
options = context.options
original = pikepdf.open(context.origin)
docinfo = get_docinfo(original, options)
pdf = pikepdf.open(working_file)
with pdf.open_metadata() as meta:
meta.load_from_docinfo(docinfo, delete_missing=False)
# If xmp:CreateDate is missing, set it to the modify date to
# match Ghostscript, for consistency
if 'xmp:CreateDate' not in meta:
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
meta_original = original.open_metadata()
not_copied = set(meta_original.keys()) - set(meta.keys())
if not_copied:
if options.output_type.startswith('pdfa'):
context.log.warning(
"Some input metadata could not be copied because it is not "
"permitted in PDF/A. You may wish to examine the output "
"PDF's XMP metadata."
)
context.log.debug(
"The following metadata fields were not copied: %r", not_copied
)
else:
context.log.error(
"Some input metadata could not be copied."
"You may wish to examine the output PDF's XMP metadata."
)
context.log.info(
"The following metadata fields were not copied: %r", not_copied
)
pdf.save(
output_file,
compress_streams=True,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
linearize=( # Don't linearize if optimize() will be linearizing too
should_linearize(working_file, context) if options.optimize == 0 else False
),
)
original.close()
pdf.close()
def report_on_metadata(missing):
if not missing:
return
if options.output_type.startswith('pdfa'):
context.log.warning(
"Some input metadata could not be copied because it is not "
"permitted in PDF/A. You may wish to examine the output "
"PDF's XMP metadata."
)
context.log.debug(
"The following metadata fields were not copied: %r", missing
)
else:
context.log.error(
"Some input metadata could not be copied."
"You may wish to examine the output PDF's XMP metadata."
)
context.log.info(
"The following metadata fields were not copied: %r", missing
)
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
docinfo = get_docinfo(original, options)
with pdf.open_metadata() as meta:
meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
# If xmp:CreateDate is missing, set it to the modify date to
# match Ghostscript, for consistency
if 'xmp:CreateDate' not in meta:
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
meta_original = original.open_metadata()
missing = set(meta_original.keys()) - set(meta.keys())
report_on_metadata(missing)
pdf.save(
output_file,
compress_streams=True,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
linearize=( # Don't linearize if optimize() will be linearizing too
should_linearize(working_file, context)
if options.optimize == 0
else False
),
)
return output_file
+59 -22
View File
@@ -23,6 +23,7 @@ import signal
import sys
import threading
from collections import namedtuple
from pathlib import Path
from tempfile import mkdtemp
import PIL
@@ -267,28 +268,43 @@ def exec_concurrent(context):
unit='page',
unit_scale=0.5,
disable=not context.options.progress_bar,
) as pbar, Pool(
processes=max_workers,
initializer=initializer,
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
) as pool:
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
while True:
try:
page_result = results.next()
sidecars[page_result.pageno] = page_result.text
pbar.update()
ocrgraft.graft_page(page_result)
pbar.update()
except StopIteration:
break
except (Exception, KeyboardInterrupt):
) as pbar:
pool = Pool(
processes=max_workers,
initializer=initializer,
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
)
try:
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
while True:
try:
page_result = results.next()
sidecars[page_result.pageno] = page_result.text
pbar.update()
ocrgraft.graft_page(page_result)
pbar.update()
except StopIteration:
break
except KeyboardInterrupt:
# Terminate pool so we exit instantly
pool.terminate()
# Don't try listener.join() here, will deadlock
raise
except Exception:
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
# Unless inside pytest, exit immediately because no one wants
# to wait for child processes to finalize results that will be
# thrown away. Inside pytest, we want child processes to exit
# cleanly so that they output an error messages or coverage data
# we need from them.
pool.terminate()
log_queue.put_nowait(None) # Terminate log listener
# Don't try listener.join() here, will deadlock
raise
raise
finally:
# Terminate log listener
log_queue.put_nowait(None)
pool.close()
pool.join()
log_queue.put_nowait(None)
listener.join()
# Output sidecar text
@@ -320,6 +336,17 @@ def samefile(f1, f2):
return os.path.samefile(f1, f2)
def configure_debug_logging(log_filename, prefix=''):
log_file_handler = logging.FileHandler(log_filename, delay=True)
log_file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'[%(asctime)s] - %(name)s - %(levelname)7s - %(message)s'
)
log_file_handler.setFormatter(formatter)
logging.getLogger(prefix).addHandler(log_file_handler)
return log_file_handler
def run_pipeline(options, api=False):
log = make_logger(options, __name__)
@@ -330,13 +357,22 @@ def run_pipeline(options, api=False):
options.jobs = available_cpu_count()
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
'PYTEST_CURRENT_TEST', ''
):
configure_debug_logging(Path(work_folder) / "debug.log")
try:
check_requested_output_file(options)
start_input_file = create_input_file(options, work_folder)
start_input_file, original_filename = create_input_file(options, work_folder)
# Triage image or pdf
origin_pdf = triage(
start_input_file, os.path.join(work_folder, 'origin.pdf'), options, log
original_filename,
start_input_file,
os.path.join(work_folder, 'origin.pdf'),
options,
log,
)
# Gather pdfinfo and create context
@@ -345,6 +381,7 @@ def run_pipeline(options, api=False):
detailed_page_analysis=options.redo_ocr,
progbar=options.progress_bar,
)
context = PDFContext(options, work_folder, origin_pdf, pdfinfo)
# Validate options are okay for this pdf
+3 -3
View File
@@ -314,7 +314,7 @@ def check_options(options):
check_dependency_versions(options)
def check_closed_streams(options):
def check_closed_streams(options): # pragma: no cover
"""Work around Python issue with multiprocessing forking on closed streams
https://bugs.python.org/issue28326
@@ -380,12 +380,12 @@ def create_input_file(options, work_folder):
target = os.path.join(work_folder, 'stdin')
with open(target, 'wb') as stream_buffer:
copyfileobj(sys.stdin.buffer, stream_buffer)
return target
return target, "<stdin>"
else:
try:
target = os.path.join(work_folder, 'origin')
safe_symlink(options.input_file, target)
return target
return target, os.fspath(options.input_file)
except FileNotFoundError:
raise InputFileError(f"File not found - {options.input_file}")
+18 -7
View File
@@ -18,10 +18,10 @@
import logging
import os
import sys
import warnings
from contextlib import suppress
from enum import IntEnum
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List
from tqdm import tqdm
@@ -31,7 +31,15 @@ from .cli import parser
class TqdmConsole:
"""Wrapper to log messages in a way that is compatible with tqdm progress bar"""
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
This routes log messages through tqdm so that it can print them above the
progress bar, and then refresh the progress bar, rather than overwriting
it which looks messy.
For some reason Python 3.6 prints extra empty messages from time to time,
so we suppress those.
"""
def __init__(self, file):
self.file = file
@@ -46,7 +54,7 @@ class TqdmConsole:
tqdm.write(msg.rstrip(), end='\n', file=self.file)
def flush(self):
if hasattr(self.file, "flush"):
with suppress(AttributeError):
self.file.flush()
@@ -80,11 +88,14 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
overwrite the progress bar
manage_root_logger (bool): Configure the process's root logger, to ensure
all log output is sent through
Returns:
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
"""
prefix = '' if manage_root_logger else 'ocrmypdf'
log = logging.getLogger(prefix)
log.setLevel(logging.INFO)
log.setLevel(logging.DEBUG)
if progress_bar_friendly:
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
@@ -99,8 +110,6 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
console.setLevel(logging.INFO)
formatter = logging.Formatter('%(levelname)7s - %(message)s')
if verbosity >= 1:
log.setLevel(logging.DEBUG)
if verbosity >= 2:
formatter = logging.Formatter('%(name)s - %(levelname)7s - %(message)s')
@@ -116,6 +125,8 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
if manage_root_logger:
logging.captureWarnings(True)
return log
def create_options(*, input_file, output_file, **kwargs):
cmdline = []
+12 -3
View File
@@ -29,7 +29,7 @@ from subprocess import run as subprocess_run
from ..exceptions import ExitCode, MissingDependencyError
log = logging.Logger(__name__)
log = logging.getLogger(__name__)
def _get_program(args, env=None):
@@ -77,12 +77,21 @@ def run(args, *, env=None, **kwargs):
if new_args0:
args[0] = new_args0
log.debug(args)
process_log = log.getChild(os.path.basename(program))
process_log.debug("Running: %s", args)
if sys.version_info < (3, 7) and os.name == 'nt':
# Can't use close_fds=True on Windows with Python 3.6 or older
# https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False
return subprocess_run(args, env=env, **kwargs)
proc = subprocess_run(args, env=env, **kwargs)
if process_log.isEnabledFor(logging.DEBUG):
try:
stderr = proc.stderr.decode('utf-8', 'replace')
except AttributeError:
stderr = proc.stderr
if stderr:
process_log.debug("stderr = %s", stderr)
return proc
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
-7
View File
@@ -195,8 +195,6 @@ def rasterize_pdf(
try:
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
except CalledProcessError as e:
with suppress(OSError):
Path(output_file).unlink() # no unfinished files
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript rasterizing failed')
else:
@@ -313,15 +311,12 @@ def generate_pdfa(
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
log.debug(args_gs)
try:
with Path(output_file).open('wb') as output:
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
# PDF/A - check PDF/A status elsewhere
with suppress(OSError):
Path(output_file).unlink()
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
else:
@@ -346,5 +341,3 @@ def generate_pdfa(
"Ghostscript had to remove PDF 'overprinting' from the "
"input file to complete PDF/A conversion. "
)
else:
log.debug(stderr)
+2 -7
View File
@@ -260,8 +260,8 @@ def generate_hocr(
log,
):
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
output_hocr = next(o for o in output_files if fspath(o).endswith('.hocr'))
output_sidecar = next(o for o in output_files if fspath(o).endswith('.txt'))
prefix = os.path.splitext(output_hocr)[0]
args_tesseract = tess_base_args(language, engine_mode)
@@ -275,14 +275,10 @@ def generate_hocr(
if user_patterns:
args_tesseract.extend(['--user-patterns', user_patterns])
if user_words or user_patterns:
args_tesseract.extend(['-c', 'lstm_use_matrix=1'])
# Reminder: test suite tesseract spoofers will break after any changes
# to the number of order parameters here
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
p = run(
args_tesseract,
stdout=PIPE,
@@ -381,7 +377,6 @@ def generate_pdf(
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
p = run(
args_tesseract,
stdout=PIPE,
+29 -21
View File
@@ -104,28 +104,36 @@ def is_file_writable(test_file):
can replace it atomically. Before doing the OCR work, make sure
the location is writable.
"""
p = Path(test_file)
if p.is_symlink():
p = p.resolve(strict=False)
# p.is_file() throws an exception in some cases
if p.exists() and p.is_file():
return os.access(
os.fspath(p),
os.W_OK,
effective_ids=(os.access in os.supports_effective_ids),
)
else:
try:
fp = p.open('wb')
except OSError:
return False
try:
if not isinstance(test_file, Path):
p = Path(test_file)
else:
fp.close()
with suppress(OSError):
p.unlink()
return True
p = test_file
if p.is_symlink():
p = p.resolve(strict=False)
# p.is_file() throws an exception in some cases
if p.exists() and p.is_file():
return os.access(
os.fspath(p),
os.W_OK,
effective_ids=(os.access in os.supports_effective_ids),
)
else:
try:
fp = p.open('wb')
except OSError:
return False
else:
fp.close()
with suppress(OSError):
p.unlink()
return True
except (EnvironmentError, RuntimeError) as e:
log.debug(e)
log.error(str(e))
return False
def deprecated(func):
+5 -5
View File
@@ -88,7 +88,7 @@ class HocrTransform:
if self.width is None or self.height is None:
raise HocrTransformError("hocr file is missing page dimensions")
def __str__(self):
def __str__(self): # pragma: no cover
"""
Return the textual content of the HTML body
"""
@@ -190,7 +190,7 @@ class HocrTransform:
pt = self.pt_from_pixel(pxl_coords)
# draw the bbox border
if showBoundingboxes:
if showBoundingboxes: # pragma: no cover
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
)
@@ -231,7 +231,7 @@ class HocrTransform:
pdf.save()
@classmethod
def polyval(cls, poly, x):
def polyval(cls, poly, x): # pragma: no cover
return x * poly[0] + poly[1]
def _do_line(
@@ -269,7 +269,7 @@ class HocrTransform:
# of the line box
baseline_y2 = self.height - (line_box.y2 + intercept)
if showBoundingboxes:
if showBoundingboxes: # pragma: no cover
# draw the baseline in magenta, dashed
pdf.setDash()
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
@@ -318,7 +318,7 @@ class HocrTransform:
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
# draw the bbox border
if showBoundingboxes:
if showBoundingboxes: # pragma: no cover
pdf.rect(
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
)
+18 -7
View File
@@ -47,21 +47,32 @@ if os.name == 'nt':
else:
libname = 'lept'
_libpath = find_library(libname)
if not _libpath and os.name == 'nt':
if not _libpath:
raise MissingDependencyError(
"""
---------------------------------------------------------------------
This error normally occurs when ocrmypdf can't find a file named
liblept-5.dll (Leptonica). Please ensure Tesseract-OCR is installed
and its location is added to the system PATH environment variable.
This error normally occurs when ocrmypdf can't find the Leptonica
library, which is usually installed with Tesseract OCR. It could be that
Tesseract is not installed properly, we can't find the installation
on your system PATH environment variable.
For details see:
The library we are looking for is usually called:
liblept-5.dll (Windows)
liblept*.dylib (macOS)
liblept*.so (Linux/BSD)
Please review our installation procedures to find a solution:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
)
lept = ffi.dlopen(_libpath)
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
try:
lept = ffi.dlopen(_libpath)
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
except ffi.error as e:
raise MissingDependencyError(
f"Leptonica library found at {_libpath}, but we could not access it"
) from e
class _LeptonicaErrorTrap:
+60
View File
@@ -0,0 +1,60 @@
# © 2020 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import re
from typing import Iterable
"""Utilities to measure OCR quality"""
class OcrQualityDictionary:
"""Manages a dictionary for simple OCR quality checks."""
def __init__(self, *, wordlist: Iterable[str] = []):
"""Construct a dictionary from a list of words.
Words for which capitalization is important should be capitalized in the
dictionary. Words that contain spaces or other punctuation will never match.
"""
self.dictionary = set()
self.dictionary.update(w for w in wordlist)
def measure_words_matched(self, ocr_text: str) -> float:
"""Check how many unique words in the OCR text match a dictionary.
Words with mixed capitalized are only considered a match if the test word
matches that capitalization.
Returns:
number of words that match / number
"""
text = re.sub(r"[0-9_]+", ' ', ocr_text)
text = re.sub(r'\W+', ' ', text)
text_words_list = re.split(r'\s+', text)
text_words = {w for w in text_words_list if len(w) >= 3}
matches = 0
for w in text_words:
if w in self.dictionary or (
w != w.lower() and w.lower() in self.dictionary
):
matches += 1
if matches > 0:
hit_ratio = matches / len(text_words)
else:
hit_ratio = 0.0
return hit_ratio
+19 -10
View File
@@ -28,12 +28,6 @@ from ocrmypdf import api, cli
pytest_plugins = ['helpers_namespace']
try:
from pytest_cov.embed import cleanup_on_sigterm
except ImportError:
pass
else:
cleanup_on_sigterm()
# pylint: disable=E1101
# pytest.helpers is dynamic so it confuses pylint
@@ -137,12 +131,12 @@ def spoof(tmp_path_factory, **kwargs):
return env
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_tesseract_noop(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_tesseract_cache(tmp_path_factory):
if running_in_docker():
return os.environ.copy()
@@ -201,7 +195,10 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
@pytest.helpers.register
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
"Run ocrmypdf and let caller deal with results"
"""Run ocrmypdf via API and let caller deal with results
Does not currently have a way to manipulate the PATH except for Tesseract.
"""
options = cli.parser.parse_args(
[str(input_file), str(output_file)]
@@ -211,6 +208,10 @@ def run_ocrmypdf_api(input_file, output_file, *args, env=None):
if env:
options.tesseract_env = env.copy()
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
first_path = env.get('_OCRMYPDF_TEST_PATH', '').split(os.pathsep)[0]
if 'spoof' in first_path:
assert 'gs' not in first_path, "use run_ocrmypdf() for gs"
assert 'tesseract' in first_path
if options.tesseract_env:
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
@@ -222,13 +223,21 @@ def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=Tr
"Run ocrmypdf and let caller deal with results"
if env is None:
env = os.environ
env = os.environ.copy()
p_args = (
OCRMYPDF
+ [str(arg) for arg in args if arg is not None]
+ [str(input_file), str(output_file)]
)
# Tell subprocess where to find coverage.py configuration
# This has no effect except when coverage is running
# Details: https://coverage.readthedocs.io/en/coverage-5.0/subprocess.html
coverage_rc = Path(__file__).parent.parent / '.coveragerc'
assert coverage_rc.exists()
env['COVERAGE_PROCESS_START'] = os.fspath(coverage_rc)
p = run(
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
)
Binary file not shown.
+3 -3
View File
@@ -35,13 +35,13 @@ def main():
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# For any rendering calls (device == pdfwrite) call real ghostscript
if '-sDEVICE=pdfwrite' in sys.argv:
# For non-image rastering calls, use real ghostscript
if '-sDEVICE=pdfwrite' in sys.argv or '-sDEVICE=txtwrite' in sys.argv:
real_ghostscript(sys.argv)
return
# Fail
print("ERROR: Ghost story archive not found")
print("ERROR: Ghost story archive not found", file=sys.stderr)
sys.exit(1)
+1 -1
View File
@@ -40,7 +40,7 @@ def main():
return
# Fail
print("ERROR: Casper is not a friendly ghost")
print("ERROR: Casper is not a friendly ghost", file=sys.stderr)
sys.exit(1)
+6 -7
View File
@@ -32,9 +32,10 @@ In orientation check mode, report the orientation is upright.
"""
import sys
from pathlib import Path
import img2pdf
import PyPDF2 as pypdf
import pikepdf
from PIL import Image
VERSION_STRING = '''tesseract 4.0.0
@@ -99,12 +100,10 @@ def main():
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
ptsize = pagesize[0] * 72, pagesize[1] * 72
pdf_out = pypdf.PdfFileWriter()
pdf_out.addBlankPage(ptsize[0], ptsize[1])
with open(output + '.pdf', 'wb') as f:
pdf_out.write(f)
with open(output + '.txt', 'w') as f:
f.write('')
pdf_out = pikepdf.new()
pdf_out.add_blank_page(page_size=ptsize)
pdf_out.save(Path(output).with_suffix('.pdf'), static_id=True)
Path(output).with_suffix('.txt').write_text('')
else:
inputf = sys.argv[-4]
output = sys.argv[-3]
+10 -1
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import pytest
import ocrmypdf
@@ -30,4 +32,11 @@ def acroform(resources):
def test_acroform_and_redo(acroform, caplog, no_outpdf):
with pytest.raises(ocrmypdf.exceptions.InputFileError):
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
assert '--redo-ocr is not currently possible' in caplog.text
assert '--redo-ocr is not currently possible' in caplog.text
def test_acroform_message(acroform, caplog, spoof_tesseract_noop, outpdf):
caplog.set_level(logging.INFO)
check_ocrmypdf(acroform, outpdf, env=spoof_tesseract_noop)
assert 'fillable form' in caplog.text
assert '--force-ocr' in caplog.text
+61
View File
@@ -0,0 +1,61 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
from io import StringIO
import pytest
from tqdm import tqdm
import ocrmypdf
def test_raw_console():
bio = StringIO()
tqconsole = ocrmypdf.api.TqdmConsole(file=bio)
tqconsole.write("Test")
tqconsole.flush()
assert "Test" in bio.getvalue()
def test_tqdm_console():
log = logging.getLogger()
log.setLevel(logging.INFO)
formatter = logging.Formatter('%(message)s')
bio = StringIO()
console = logging.StreamHandler(ocrmypdf.api.TqdmConsole(file=bio))
console.setFormatter(formatter)
log.addHandler(console)
def before_pbar(message):
# Ensure that log messages appear before the progress bar, even when
# printed after the progress bar updates.
v = bio.getvalue()
pbar_start_marker = '|#'
return v.index(message) < v.index(pbar_start_marker)
with tqdm(total=2, file=bio, disable=False) as pbar:
pbar.update()
msg = "1/2 above progress bar"
log.info(msg)
assert before_pbar(msg)
log.info("done")
assert not before_pbar("done")
+23 -23
View File
@@ -31,28 +31,28 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
spoof = pytest.helpers.spoof
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_no_tess_gs_render_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
)
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
)
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_no_tess_no_pdfa(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
)
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_no_tess_pdfa_warning(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
@@ -60,18 +60,18 @@ def spoof_no_tess_pdfa_warning(tmp_path_factory):
@pytest.fixture
def linn(resources):
path = resources / 'linn.pdf'
def francais(resources):
path = resources / 'francais.pdf'
return path, pikepdf.open(path)
def test_rasterize_size(linn, outdir, caplog):
path, pdf = linn
def test_rasterize_size(francais, outdir, caplog):
path, pdf = francais
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
target_size = Decimal('200.0'), Decimal('150.0')
target_dpi = 42.0, 4242.0
target_size = Decimal('50.0'), Decimal('30.0')
forced_dpi = 42.0, 4242.0
log = logging.getLogger()
rasterize_pdf(
@@ -81,21 +81,21 @@ def test_rasterize_size(linn, outdir, caplog):
target_size[1] / page_size[1],
raster_device='pngmono',
log=log,
page_dpi=target_dpi,
page_dpi=forced_dpi,
)
with Image.open(outdir / 'out.png') as im:
assert im.size == target_size
assert im.info['dpi'] == target_dpi
assert im.info['dpi'] == forced_dpi
def test_rasterize_rotated(linn, outdir, caplog):
path, pdf = linn
def test_rasterize_rotated(francais, outdir, caplog):
path, pdf = francais
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
target_size = Decimal('200.0'), Decimal('150.0')
target_dpi = 42.0, 4242.0
target_size = Decimal('50.0'), Decimal('30.0')
forced_dpi = 42.0, 4242.0
log = logging.getLogger()
caplog.set_level(logging.DEBUG)
@@ -106,34 +106,34 @@ def test_rasterize_rotated(linn, outdir, caplog):
target_size[1] / page_size[1],
raster_device='pngmono',
log=log,
page_dpi=target_dpi,
page_dpi=forced_dpi,
rotation=90,
)
with Image.open(outdir / 'out.png') as im:
assert im.size == (target_size[1], target_size[0])
assert im.info['dpi'] == (target_dpi[1], target_dpi[0])
assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0])
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
)
print(err)
assert 'Casper is not a friendly ghost' in err
assert p.returncode == ExitCode.child_process_error
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
resources / 'francais.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
)
print(err)
assert 'Ghost story archive not found' in err
assert p.returncode == ExitCode.child_process_error
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
resources / 'francais.pdf', outpdf, env=spoof_no_tess_no_pdfa
)
assert (
p.returncode == ExitCode.pdfa_conversion_failed
@@ -141,4 +141,4 @@ def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
check_ocrmypdf(resources / 'francais.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
+97
View File
@@ -0,0 +1,97 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import multiprocessing
from pathlib import Path
from unittest.mock import MagicMock
import pytest
import ocrmypdf.helpers as helpers
class TestSafeSymlink:
def test_safe_symlink_link_self(self, tmp_path, caplog):
helpers.safe_symlink(tmp_path / 'self', tmp_path / 'self')
assert caplog.record_tuples[0][1] == logging.WARNING
def test_safe_symlink_overwrite(self, tmp_path):
(tmp_path / 'regular_file').touch()
with pytest.raises(FileExistsError):
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
def test_safe_symlink_relink(self, tmp_path):
(tmp_path / 'regular_file_a').touch()
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
(tmp_path / 'link').symlink_to(tmp_path / 'regular_file_a')
helpers.safe_symlink(tmp_path / 'regular_file_b', tmp_path / 'link')
assert (tmp_path / 'link').samefile(tmp_path / 'regular_file_b') or (
tmp_path / 'link'
).read_bytes() == b'ABC'
def test_no_cpu_count(monkeypatch):
def cpu_count_raises():
raise NotImplementedError()
monkeypatch.setattr(multiprocessing, 'cpu_count', cpu_count_raises)
with pytest.warns(expected_warning=UserWarning):
assert helpers.available_cpu_count() == 1
def test_deprecated():
@helpers.deprecated
def old_function():
return 42
with pytest.deprecated_call():
assert old_function() == 42
class TestFileIsWritable:
@pytest.fixture
def non_existent(self, tmp_path):
return tmp_path / 'nofile'
@pytest.fixture
def basic_file(self, tmp_path):
basic = tmp_path / 'basic'
basic.touch()
return basic
def test_plain(self, non_existent):
assert helpers.is_file_writable(non_existent)
def test_symlink_loop(self, tmp_path):
loop = tmp_path / 'loop'
loop.symlink_to(loop)
assert not helpers.is_file_writable(loop)
def test_chmod(self, basic_file):
assert helpers.is_file_writable(basic_file)
basic_file.chmod(0o400)
assert not helpers.is_file_writable(basic_file)
basic_file.chmod(0o000)
assert not helpers.is_file_writable(basic_file)
def test_permission_error(self, basic_file):
pathmock = MagicMock(spec_set=basic_file)
pathmock.is_symlink.return_value = False
pathmock.exists.return_value = True
pathmock.is_file.side_effect = PermissionError
assert not helpers.is_file_writable(pathmock)
+12 -2
View File
@@ -45,12 +45,12 @@ spoof = pytest.helpers.spoof
RENDERERS = ['hocr', 'sandwich']
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_tesseract_crash(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_crash.py')
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_tesseract_big_image_error(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
@@ -270,6 +270,16 @@ def test_input_file_not_found(caplog, no_outpdf):
assert input_file in caplog.text
@pytest.mark.skipif(os.name == 'nt', reason="chmod")
def test_input_file_not_readable(caplog, resources, outdir, no_outpdf):
input_file = outdir / 'trivial.pdf'
shutil.copy(resources / 'trivial.pdf', input_file)
input_file.chmod(0o000)
result = run_ocrmypdf_api(input_file, no_outpdf)
assert result == ExitCode.input_file
assert str(input_file) in caplog.text
def test_input_file_not_a_pdf(caplog, no_outpdf):
input_file = __file__ # Try to OCR this file
result = run_ocrmypdf_api(input_file, no_outpdf)
+35
View File
@@ -0,0 +1,35 @@
# © 2020 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import pytest
import ocrmypdf.quality as qual
def test_quality_measurement():
oqd = qual.OcrQualityDictionary(
wordlist=["words", "words", "quick", "brown", "fox", "dog", "lazy"]
)
assert len(oqd.dictionary) == 6 # 6 unique
assert (
oqd.measure_words_matched("The quick brown fox jumps quickly over the lazy dog")
== 0.5
)
assert oqd.measure_words_matched("12345 10% _f 7fox -brown | words") == 1.0
assert oqd.measure_words_matched("quick quick quick") == 1.0
+7 -1
View File
@@ -33,7 +33,7 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
@pytest.fixture(scope='session')
@pytest.fixture
def spoof_tess_bad_utf8(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
@@ -56,6 +56,9 @@ def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
pytest.skip(msg="Coverage uses stdout")
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
@@ -121,6 +124,9 @@ def test_bad_locale():
reason="Windows does not like this; not sure how to fix",
)
def test_dev_null(spoof_tesseract_noop, resources):
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
pytest.skip(msg="Coverage uses stdout")
p, out, err = run_ocrmypdf(
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
)
+94
View File
@@ -15,7 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import subprocess
from contextlib import contextmanager
from os import fspath
from pathlib import Path
@@ -81,3 +83,95 @@ def test_no_languages(tmp_path):
with pytest.raises(MissingDependencyError):
tesseract.languages(tesseract_env=env)
def test_image_too_large_hocr(monkeypatch, resources, outdir):
log = logging.getLogger('test_image_too_large_hocr')
def dummy_run(args, *, env=None, **kwargs):
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
monkeypatch.setattr(tesseract, 'run', dummy_run)
tesseract.generate_hocr(
input_file=resources / 'crom.png',
output_files=[outdir / 'out.hocr', outdir / 'out.txt'],
language=['eng'],
engine_mode=None,
tessconfig=[],
timeout=180.0,
pagesegmode=None,
log=log,
user_words=None,
user_patterns=None,
tesseract_env=None,
)
assert "name='ocr-capabilities'" in Path(outdir / 'out.hocr').read_text()
def test_image_too_large_pdf(monkeypatch, resources, outdir):
log = logging.getLogger('test_image_too_large_pdf')
def dummy_run(args, *, env=None, **kwargs):
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
monkeypatch.setattr(tesseract, 'run', dummy_run)
tesseract.generate_pdf(
input_image=resources / 'crom.png',
skip_pdf=resources / 'blank.pdf',
output_pdf=outdir / 'pdf.pdf',
output_text=outdir / 'txt.txt',
language=['eng'],
engine_mode=None,
text_only=False,
tessconfig=[],
timeout=180.0,
pagesegmode=None,
log=log,
user_words=None,
user_patterns=None,
tesseract_env=None,
)
assert Path(outdir / 'txt.txt').read_text() == '[skipped page]'
if os.name != 'nt': # different semantics
assert Path(outdir / 'pdf.pdf').samefile(resources / 'blank.pdf')
def test_timeout(caplog):
log = logging.getLogger('test_timeout')
tesseract.page_timedout(log, '123456.png', 5)
assert "123456" in caplog.text
assert "took too long" in caplog.text
@pytest.mark.parametrize(
'in_, logged',
[
(b'Tesseract Open Source', ''),
(b'lots of diacritics blah blah', 'diacritics'),
(b'Warning in pixReadMem', ''),
(b'OSD: Weak margin', 'unsure about page orientation'),
(b'Error in pixScanForForeground', ''),
(b'Error in boxClipToRectangle', ''),
(b'an unexpected error', 'an unexpected error'),
(b'a dire warning', 'a dire warning'),
(b'read_params_file something', 'read_params_file'),
(b'an innocent message', 'innocent'),
(b'\x7f\x7f\x80innocent unicode failure', 'innocent'),
],
)
def test_tesseract_log_output(caplog, in_, logged):
log = logging.getLogger('tesseract_log_output')
log.setLevel(logging.INFO)
tesseract.tesseract_log_output(log, in_, 'dummy')
if logged == '':
assert caplog.text == ''
else:
assert logged in caplog.text
def test_tesseract_log_output_raises(caplog):
log = logging.getLogger('tesseract_log_output')
with pytest.raises(tesseract.TesseractConfigError):
tesseract.tesseract_log_output(log, b'parameter not found: moo', 'dummy')
assert 'not found' in caplog.text
+1 -1
View File
@@ -43,7 +43,7 @@ def have_unpaper():
return True
@pytest.fixture(scope="session")
@pytest.fixture
def spoof_unpaper_oldversion(tmp_path_factory):
return spoof(tmp_path_factory, unpaper="unpaper_oldversion.py")