diff --git a/README.md b/README.md index 31c5cd2..342fa7e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ BarCode(raw='This should be QR_CODE', parsed='This should be QR_CODE', path='tes The attributes of the decoded `BarCode` object are `raw`, `parsed`, `path`, `format`, `type`, and `points`. The list of formats which ZXing can decode is [here](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). -The `decode()` method accepts an image path (or list of paths) and takes optional parameters `try_harder` (boolean), `possible_formats` (list of formats to consider), and `pure_barcode` (boolean). +The `decode()` method accepts an image path or [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html) (or list thereof) +and takes optional parameters `try_harder` (boolean), `possible_formats` (list of formats to consider), and `pure_barcode` (boolean). If no barcode is found, it returns a `False`-y `BarCode` object with all fields except `path` set to `None`. If it encounters any other recognizable error from the Java ZXing library, it raises `BarCodeReaderException`. diff --git a/requirements-test.txt b/requirements-test.txt index 3de01b6..e56972b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1 +1,5 @@ nose>=1.0 + +pillow>=3.0,<6.0; python_version < '3.5' +pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6' +pillow>=8.0; python_version >= '3.6' diff --git a/setup.py b/setup.py index aa3d492..a6c5113 100644 --- a/setup.py +++ b/setup.py @@ -46,13 +46,21 @@ def download_java_files(force=False): name='zxing', version=version_pep, description="Wrapper for decoding/reading barcodes with ZXing (Zebra Crossing) library", - long_description="More information: https://github.com/dlenski/python-zxing", + long_description=open('README.md').read(), + long_description_content_type='text/markdown', url="https://github.com/dlenski/python-zxing", author='Daniel Lenski', author_email='dlenski@gmail.com', packages=['zxing'], package_data={'zxing': download_java_files()}, entry_points={'console_scripts': ['zxing=zxing.__main__:main']}, + extras_require={ + "Image": [ + "pillow>=3.0,<6.0; python_version < '3.5'", + "pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6'", + "pillow>=8.0; python_version >= '3.6'", + ] + }, install_requires=open('requirements.txt').readlines(), tests_require=open('requirements-test.txt').readlines(), test_suite='nose.collector', diff --git a/test/test_all.py b/test/test_all.py index b377458..dfa6415 100644 --- a/test/test_all.py +++ b/test/test_all.py @@ -2,6 +2,8 @@ import os from tempfile import mkdtemp +from PIL import Image + from nose import with_setup from nose.tools import raises @@ -43,11 +45,12 @@ def test_version(): @with_setup(setup_reader) -def _check_decoding(filename, expected_format, expected_raw, extra={}): +def _check_decoding(filename, expected_format, expected_raw, extra={}, as_Image=False): global test_reader path = os.path.join(test_barcode_dir, filename) + what = Image.open(path) if as_Image else path logging.debug('Trying to parse {}, expecting {!r}.'.format(path, expected_raw)) - dec = test_reader.decode(path, pure_barcode=True, **extra) + dec = test_reader.decode(what, pure_barcode=True, **extra) if expected_raw is None: assert dec.raw is None, ( 'Expected failure, but got result in {} format'.format(expected_format, dec.format)) @@ -56,6 +59,9 @@ def _check_decoding(filename, expected_format, expected_raw, extra={}): 'Expected {!r} but got {!r}'.format(expected_raw, dec.raw)) assert dec.format == expected_format, ( 'Expected {!r} but got {!r}'.format(expected_format, dec.format)) + if as_Image: + assert not os.path.exists(dec.path), ( + 'Expected temporary file {!r} to be deleted, but it still exists'.format(dec.path)) def test_decoding(): @@ -63,6 +69,11 @@ def test_decoding(): yield from ((_check_decoding, filename, expected_format, expected_raw) for filename, expected_format, expected_raw in test_valid_images) +def test_decoding_from_Image(): + global test_reader + yield from ((_check_decoding, filename, expected_format, expected_raw, {}, True) for filename, expected_format, expected_raw in test_valid_images) + + def test_possible_formats(): yield from ((_check_decoding, filename, expected_format, expected_raw, dict(possible_formats=('CODE_93', expected_format, 'DATA_MATRIX'))) for filename, expected_format, expected_raw in test_barcodes) diff --git a/zxing/__init__.py b/zxing/__init__.py index 428f06f..e883fb2 100644 --- a/zxing/__init__.py +++ b/zxing/__init__.py @@ -14,6 +14,13 @@ import zipfile from enum import Enum +try: + from PIL.Image import Image + from tempfile import NamedTemporaryFile + have_pil = True +except ImportError: + have_pil = None + from .version import __version__ # noqa: F401 @@ -53,12 +60,24 @@ def __init__(self, classpath=None, java=None): def decode(self, filenames, try_harder=False, possible_formats=None, pure_barcode=False, products_only=False): possible_formats = (possible_formats,) if isinstance(possible_formats, str) else possible_formats - if isinstance(filenames, str): + if isinstance(filenames, (str, Image) if have_pil else str): one_file = True filenames = filenames, else: one_file = False - file_uris = [pathlib.Path(f).absolute().as_uri() for f in filenames] + + file_uris = [] + temp_files = [] + for fn_or_im in filenames: + if have_pil and isinstance(fn_or_im, Image): + tf = NamedTemporaryFile(prefix='PIL_image_', suffix='.png') + temp_files.append(tf) + fn_or_im.save(tf, compresslevel=0) + tf.flush() + fn = tf.name + else: + fn = fn_or_im + file_uris.append(pathlib.Path(fn).absolute().as_uri()) cmd = [self.java, '-cp', self.classpath, self.cls] + file_uris if try_harder: @@ -75,7 +94,11 @@ def decode(self, filenames, try_harder=False, possible_formats=None, pure_barcod p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT, universal_newlines=False) except OSError as e: raise BarCodeReaderException("Could not execute specified Java binary", self.java) from e - stdout, stderr = p.communicate() + else: + stdout, stderr = p.communicate() + finally: + for tf in temp_files: + tf.close() if stdout.startswith((b'Error: Could not find or load main class com.google.zxing.client.j2se.CommandLineRunner', b'Exception in thread "main" java.lang.NoClassDefFoundError:')):