Skip to content
This repository was archived by the owner on Jan 27, 2026. It is now read-only.

Commit a2f16b1

Browse files
committed
Merge branch 'develop' into master
2 parents dd12827 + c78cdb0 commit a2f16b1

9 files changed

Lines changed: 78 additions & 51 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ Version 1.3.0
55
--------------
66

77
* Handprint now requires Python version 3.6 or later.
8+
* Fixed issue [#19](https://github.com/caltechlibrary/handprint/issues/19), which caused Handprint to fail to produce any output images if both `-e` and `-G` were given.
9+
* Fixed warning about Matlplotlib GUIs and threading in `images.py`.
10+
* Fixed macOS Mojave compatibility (issue [#16](https://github.com/caltechlibrary/handprint/issues/16).
811
* Updated `handprint/services/microsoft.py` to work with Azure API v. 3.0.
12+
* Updated Microsoft credentials code to allow the endpoint URI to be supplied.
913
* Changed and expanded the possible exit codes returned by Handprint. (Please see the docs for more info.)
1014
* Changed Google interface to retrieve _only_ document text results instead of all possible results, for better efficiency.
1115
* Improved handling of `^C` interrupts from the command line.
1216
* Added signal catcher to drop Handprint into `pdb` upon receiving `SIGUSER`.
13-
* Fixed warning about Matlplotlib GUIs and threading in `images.py`.
1417
* Switched to the use of [Sidetrack](https://github.com/caltechlibrary/sidetrack) for debug logging.
1518
* Switched to the use of [Rich](https://github.com/willmcgugan/rich) for terminal output
1619
* Various internal code updates and refactoring.

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ The _**Hand**written **P**age **R**ecognit**i**o**n** **T**est_ program applies
1414
❡ Log of recent changes
1515
-----------------------
1616

17-
_Version 1.2.2_: This release fixes an inconsistency (compared to other Handprint output) in the way that Microsoft's text recognition service results are drawn on annotated images. The copyright year in source files has also been updated.
18-
19-
_Version 1.2.0_: This version fixes a bug in creating annotated results images, in which results from multiple services were overwritten on top of each other. It also fixes a bug with the Amazon interface that resulted in occasional random errors about `endpoint_resolver`. This version of Handprint also changes how output files are written; the new scheme uses the naming pattern `somefile.handprint.png` for the rescaled input image, `somefile.handprint-service.png` for the various service output results, and `somefile.handprint-all.png` for the summary grid image. Handprint now also accepts PDF files as input.
17+
_Version 1.3.0_: This release brings a number of changes: (1) it now requires Python version 3.6 at minimum; (2) the Microsoft service interface now uses Azure API v3.0; (3) the Microsoft credentials scheme now allows you to change the endpoint URI; (4) the Google service interface now only gets the document text results instead of all possible results; (5) the possible program exit codes have changed slightly; and (6) interruption via <kbd>^C</kbd> should work better now. Some bugs have been fixed and internals have been (hopefully) improved.
2018

2119
The file [CHANGES](CHANGES.md) contains a more complete change log, and includes information about previous releases.
2220

@@ -89,15 +87,18 @@ _SERVICENAME_ must be one of the service names printed by running `handprint -l`
8987

9088
#### Microsoft
9189

92-
Microsoft's approach to credentials in Azure involves the use of [subscription keys](https://docs.microsoft.com/en-us/azure/cognitive-services/computer-vision/vision-api-how-to-topics/howtosubscribe). The format of the credentials file for Handprint just needs to contain a single field:
90+
Microsoft's approach to credentials in Azure involves the use of [subscription keys](https://docs.microsoft.com/en-us/azure/cognitive-services/computer-vision/vision-api-how-to-topics/howtosubscribe). The format of the credentials file for Handprint needs to contain two fields:
9391

9492
```json
9593
{
96-
"subscription_key": "YOURKEYHERE"
94+
"subscription_key": "YOURKEYHERE",
95+
"endpoint": "https://ENDPOINT"
9796
}
9897
```
9998

100-
The value of "YOURKEYHERE" will be a string such as `"18de248475134eb49ae4a4e94b93461c"`. To obtain a key, visit [https://portal.azure.com](https://portal.azure.com) and sign in using your account login. (Note: you will need to turn off browser security plugins such as Ad&nbsp;Block and uMatrix if you have them, or else the site will not work.) Once you are authenticated to the Azure portal, you can create credentials for using Azure's machine-learning services. Some notes about this can be found in the [Handprint project Wiki pages on GitHub](https://github.com/caltechlibrary/handprint/wiki/Getting-Microsoft-Azure-credentials). Once you have obtained a key, use a text editor to create a JSON file in the simple format shown above, save that file somewhere on your computer (for the sake of this example, assume it is `myazurecredentials.json`), and use the command discussed above to make Handprint copy the credentials file:
99+
The value `"YOURKEYHERE"` will be a string such as `"18de248475134eb49ae4a4e94b93461c"`, and it will be associated with an endpoint URI such as `"https://westus.api.cognitive.microsoft.com"`. To obtain a key and the corresponding endpoint URI, visit [https://portal.azure.com](https://portal.azure.com) and sign in using your account login. (Note: you will need to turn off browser security plugins such as Ad&nbsp;Block and uMatrix if you have them, or else the site will not work.) Once you are authenticated to the Azure portal, you can create credentials for using Azure's machine-learning services. Some notes all about this can be found in the [Handprint project Wiki pages on GitHub](https://github.com/caltechlibrary/handprint/wiki/Getting-Microsoft-Azure-credentials).
100+
101+
Once you have obtained both a key and an endpoint URI, use a text editor to create a JSON file in the simple format shown above, save that file somewhere on your computer (for the sake of this example, assume it is `myazurecredentials.json`), and use the command discussed above to make Handprint copy the credentials file:
101102
```sh
102103
handprint -a microsoft myazurecredentials.json
103104
```
@@ -322,7 +323,7 @@ Handprint produces color-coded diagnostic output as it runs, by default. Howeve
322323

323324
Handprint will send files to the different services in parallel, using a number of process threads equal to 1/2 of the number of cores on the computer it is running on. (E.g., if your computer has 4 cores, it will by default use at most 2 threads.) The `-t` option (`/t` on Windows) can be used to change this number.
324325

325-
If given the `-@` argument (`/@` on Windows), this program will output a detailed trace of what it is doing. The debug trace will be sent to the given destination, which can be `-` to indicate console output, or a file path to send the output to a file. Handprint will also install a signal handler that responds to signal `SIGUSR1`; if the signal is sent to the running process, it will drop Handprint into the `pdb` debugger. _Note_: It's best to use `-t 1` when attempting to use a debugger because otherwise subthreads will continue running even if the main thread is interrupted.
326+
If given the `-@` argument (`/@` on Windows), this program will output a detailed trace of what it is doing. The debug trace will be sent to the given destination, which can be `-` to indicate console output, or a file path to send the output to a file. On non-Windows platforms, Handprint will also install a signal handler that responds to signal `SIGUSR1`; if the signal is sent to the running process, it will drop Handprint into the `pdb` debugger. _Note_: It's best to use `-t 1` when attempting to use a debugger because otherwise subthreads will continue running even if the main thread is interrupted.
326327

327328
If given the `-V` option (`/V` on Windows), this program will print the version and other information, and exit without doing anything else.
328329

@@ -418,11 +419,13 @@ Handprint benefitted from feedback from several people, notably from Tommy Keswi
418419

419420
Handprint makes use of numerous open-source packages, without which it would have been effectively impossible to develop Handprint with the resources we had. I want to acknowledge this debt. In alphabetical order, the packages are:
420421

422+
* [aenum](https://pypi.org/project/aenum/) &ndash; advanced enumerations for Python
421423
* [appdirs](https://github.com/ActiveState/appdirs) &ndash; module for determining appropriate platform-specific directories
422424
* [boltons](https://github.com/mahmoud/boltons/) &ndash; package of miscellaneous Python utilities
423425
* [boto3](https://github.com/boto/boto3) &ndash; Amazon AWS SDK for Python
424426
* [colorama](https://github.com/tartley/colorama) &ndash; makes ANSI escape character sequences work under MS Windows terminals
425427
* [colored](https://gitlab.com/dslackw/colored) &ndash; library for color and formatting in terminal
428+
* [dateparser](https://pypi.org/project/dateparser/) &ndash; parse dates in almost any string format
426429
* [google-api-core, google-api-python-client, google-auth, google-auth-httplib2, google-cloud, google-cloud-vision, googleapis-common-protos, google_api_python_client](https://github.com/googleapis/google-cloud-python) &ndash; Google API libraries
427430
* [halo](https://github.com/ManrajGrover/halo) &ndash; busy-spinners for Python command-line programs
428431
* [humanize](https://github.com/jmoiron/humanize) &ndash; make numbers more easily readable by humans

handprint/__main__.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,19 @@ def main(add_creds = 'A', base_name = 'B', compare = False, no_color = False,
340340

341341
prefix = '/' if sys.platform.startswith('win') else '-'
342342
hint = f'(Hint: use {prefix}h for help.)'
343+
ui = UI('Handprint', 'HANDwritten Page RecognitIoN Test',
344+
use_color = not no_color, be_quiet = quiet)
345+
ui.start()
343346

344347
# Preprocess arguments and handle early exits -----------------------------
345348

346349
if debug != 'OUT':
347350
if __debug__: set_debug(True, debug, extra = '%(threadName)s')
348351
import faulthandler
349352
faulthandler.enable()
350-
pdb_on_signal(signal.SIGUSR1)
353+
if not sys.platform.startswith('win'):
354+
# Even with a different signal, I can't get this to work on Win.
355+
pdb_on_signal(signal.SIGUSR1)
351356

352357
# Handle arguments that involve deliberate exits.
353358

@@ -375,6 +380,7 @@ def main(add_creds = 'A', base_name = 'B', compare = False, no_color = False,
375380

376381
# Do sanity checks on some other arguments.
377382

383+
services = services_list() if services == 'S' else services.lower().split(',')
378384
if services != 'S' and not all(s in services_list() for s in services):
379385
alert_fatal(f'"{services}" is not a known services. {hint}')
380386
exit(int(ExitCode.bad_arg))
@@ -393,20 +399,17 @@ def main(add_creds = 'A', base_name = 'B', compare = False, no_color = False,
393399
# Do the real work --------------------------------------------------------
394400

395401
if __debug__: log('='*8 + f' started {timestamp()} ' + '='*8)
396-
ui = body = exception = None
402+
body = exception = None
397403
try:
398-
ui = UI('Handprint', 'HANDwritten Page RecognitIoN Test',
399-
use_color = not no_color, be_quiet = quiet)
400-
ui.start()
401404
body = MainBody(files = files,
402405
from_file = None if from_file == 'F' else from_file,
403406
output_dir = None if output_dir == 'O' else output_dir,
404407
add_creds = None if add_creds == 'A' else add_creds,
405408
base_name = 'document' if base_name == 'B' else base_name,
406409
make_grid = not no_grid,
407410
extended = extended,
411+
services = services,
408412
threads = max(1, cpu_count()//2 if threads == 'T' else int(threads)),
409-
services = services_list() if services == 'S' else services.lower().split(','),
410413
compare = 'relaxed' if (compare and relaxed) else compare)
411414
body.run()
412415
exception = body.exception
@@ -423,19 +426,20 @@ def main(add_creds = 'A', base_name = 'B', compare = False, no_color = False,
423426

424427
exit_code = ExitCode.success
425428
if exception:
426-
if type(exception) == CannotProceed:
427-
exit_code = exception.args[0]
428-
elif type(exception) in [KeyboardInterrupt, UserCancelled]:
429+
if exception[0] == CannotProceed:
430+
exit_code = exception[1].args[0]
431+
elif exception[0] in [KeyboardInterrupt, UserCancelled]:
429432
if __debug__: log(f'received {exception.__class__.__name__}')
433+
warn('Interrupted.')
430434
exit_code = ExitCode.user_interrupt
431435
else:
436+
msg = str(exception[1])
437+
alert_fatal(f'Encountered error {exception[0].__name__}: {msg}')
438+
exit_code = ExitCode.exception
432439
if __debug__:
433440
from traceback import format_exception
434-
msg = str(exception[1])
435-
details = ''.join(format_exception(*exception))
436-
alert_fatal(f'Error: {msg}')
441+
details = ''.join(format_exception(exception))
437442
logr(f'Exception: {msg}\n{details}')
438-
exit_code = ExitCode.exception
439443
else:
440444
inform('Done.')
441445
if __debug__: log('_'*8 + f' stopped {timestamp()} ' + '_'*8)

handprint/credentials/microsoft_auth.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
from .base import Credentials
2727
from .credentials_files import credentials_filename
2828

29+
30+
# Constants.
31+
# .............................................................................
32+
33+
_DEFAULT_ENDPOINT = 'https://westus.api.cognitive.microsoft.com'
34+
2935

3036
# Main class.
3137
# .............................................................................
@@ -42,7 +48,15 @@ def __init__(self):
4248
try:
4349
with open(cfile, 'r') as file:
4450
creds = json.load(file)
45-
self.credentials = creds['subscription_key']
51+
if 'endpoint' in creds:
52+
endpoint = creds['endpoint'].rstrip('/')
53+
if not endpoint.startswith('http'):
54+
endpoint = 'https://' + endpoint
55+
else:
56+
if __debug__: log('endpoint not found; using default')
57+
endpoint = _DEFAULT_ENDPOINT
58+
creds['endpoint'] = endpoint
59+
self.credentials = creds
4660
except Exception as ex:
4761
raise AuthFailure(
4862
'Unable to parse Microsoft exceptions file: {}'.format(str(ex)))

handprint/main_body.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from handprint import _OUTPUT_EXT, _OUTPUT_FORMAT
2727
from handprint.credentials import Credentials
2828
from handprint.exceptions import *
29+
from handprint.exit_codes import ExitCode
2930
from handprint.files import filename_extension, filename_basename
3031
from handprint.files import files_in_directory, filter_by_extensions
3132
from handprint.files import readable, writable, is_url
@@ -58,10 +59,6 @@ def __init__(self, **kwargs):
5859
self._manager = Manager(self.services, self.threads, self.output_dir,
5960
self.make_grid, self.compare, self.extended)
6061

61-
62-
def run(self):
63-
'''Run the main body.'''
64-
6562
# On Windows, in Python 3.6+, ^C in a terminal window does not stop
6663
# execution (at least in my environment). The following function
6764
# creates a closure with the worker object so that stop() can be called.
@@ -78,24 +75,23 @@ def ctrl_handler(event, *args):
7875
import win32api
7976
win32api.SetConsoleCtrlHandler(ctrl_handler, True)
8077

78+
79+
def run(self):
80+
'''Run the main body.'''
81+
8182
if __debug__: log('running MainBody')
8283
try:
8384
self._do_preflight()
8485
self._do_main_work()
8586
except (KeyboardInterrupt, UserCancelled) as ex:
8687
# This is the place where we land when Handprint receives a ^C.
8788
if __debug__: log(f'got {type(ex).__name__}')
88-
warn('Interrupted.')
8989
interrupt()
9090
self.stop()
91-
self.exception = ex
92-
except CannotProceed as ex:
93-
if __debug__: log(f'got CannotProceed')
94-
self.exception = (CannotProceed, ex)
91+
self.exception = sys.exc_info()
9592
except Exception as ex:
9693
if __debug__: log(f'exception in main body: {str(ex)}')
9794
self.exception = sys.exc_info()
98-
alert_fatal(f'Error occurred during execution:', details = str(ex))
9995
if __debug__: log('finished MainBody')
10096

10197

handprint/manager.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def run_services(self, item, index, base_name):
149149
return
150150

151151
# Send the file to the services and get Result tuples back.
152+
self._senders = []
152153
if self._num_threads == 1:
153154
# For 1 thread, avoid thread pool to make debugging easier.
154155
results = [self._send(image, s) for s in services]
@@ -163,6 +164,9 @@ def run_services(self, item, index, base_name):
163164
# If a service failed for some reason (e.g., a network glitch), we
164165
# get no result back. Remove empty results & go on with the rest.
165166
results = [x for x in results if x is not None]
167+
if not results:
168+
warn(f'Nothing to do for {item}')
169+
return
166170

167171
# Create grid file if requested.
168172
if self._make_grid:
@@ -202,8 +206,10 @@ def _get(self, item, base_name, index):
202206
if is_url(item):
203207
# First make sure the URL actually points to an image.
204208
if __debug__: log(f'testing if URL contains an image: {item}')
209+
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)'}
205210
try:
206-
response = urllib.request.urlopen(item)
211+
request = urllib.request.Request(item, None, headers)
212+
response = urllib.request.urlopen(request)
207213
except Exception as ex:
208214
warn(f'Skipping URL due to error: {ex}')
209215
return (None, None)
@@ -256,7 +262,6 @@ def _send(self, image, service):
256262
directory "dest_dir".
257263
'''
258264

259-
raise_for_interrupts()
260265
service_name = f'[{service.name_color()}]{service.name()}[/]'
261266
inform(f'Sending to {service_name} and waiting for response ...')
262267
last_time = timer()
@@ -272,21 +277,21 @@ def _send(self, image, service):
272277
warn(f'Continuing {service_name}')
273278
return self._send(image, service)
274279
if output.error:
275-
alert(f'{service_name} failed: {output.error}')
280+
# Sanitize the error string in case it contains '{' characters.
281+
msg = output.error.replace('{', '{{{{').replace('}', '}}}}')
282+
alert(f'{service_name} failed: {msg}')
276283
warn(f'No result from {service_name} for {relative(image.file)}')
277284
return None
278-
279-
raise_for_interrupts()
280285
inform(f'Got result from {service_name}.')
286+
raise_for_interrupts()
287+
288+
inform(f'Creating annotated image for {service_name}.')
281289
file_name = path.basename(image.file)
282290
base_path = path.join(image.dest_dir, file_name)
283-
annot_path = None
291+
annot_path = self._renamed(base_path, str(service), 'png')
284292
report_path = None
285-
if self._make_grid:
286-
annot_path = self._renamed(base_path, str(service), 'png')
287-
inform(f'Creating annotated image for {service_name}.')
288-
with self._lock:
289-
self._save(annotated_image(image.file, output.boxes, service), annot_path)
293+
with self._lock:
294+
self._save(annotated_image(image.file, output.boxes, service), annot_path)
290295
if self._extended_results:
291296
txt_file = self._renamed(base_path, str(service), 'txt')
292297
json_file = self._renamed(base_path, str(service), 'json')

handprint/services/google.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import google
2222
from google.cloud import vision_v1p3beta1 as gv
2323
from google.api_core.exceptions import PermissionDenied
24-
from google.cloud.vision import enums
25-
from google.cloud.vision import types
2624
from google.protobuf.json_format import MessageToDict
2725
import json
2826

0 commit comments

Comments
 (0)