diff --git a/pylibdmtx/__init__.py b/pylibdmtx/__init__.py index fd40215..137916b 100644 --- a/pylibdmtx/__init__.py +++ b/pylibdmtx/__init__.py @@ -1,3 +1,3 @@ """Read and write Data Matrix barcodes from Python 2 and 3.""" -__version__ = '0.1.10' +__version__ = '0.2.0' diff --git a/pylibdmtx/dmtx_library.py b/pylibdmtx/dmtx_library.py index 674c29f..1ead2be 100644 --- a/pylibdmtx/dmtx_library.py +++ b/pylibdmtx/dmtx_library.py @@ -2,12 +2,15 @@ """ import platform import sys +import os +import re from ctypes import cdll from ctypes.util import find_library from pathlib import Path __all__ = ['load'] +_lib_name = 'dmtx' def _windows_fname(): @@ -16,7 +19,7 @@ def _windows_fname(): This logic has its own function to make testing easier """ - return 'libdmtx-64.dll' if sys.maxsize > 2**32 else 'libdmtx-32.dll' + return f'lib{_lib_name}-64.dll' if sys.maxsize > 2**32 else f'lib{_lib_name}-32.dll' def load(): @@ -42,7 +45,15 @@ def load(): ) else: # Assume a shared library on the path - path = find_library('dmtx') + path = find_library(_lib_name) + if not path: + # Search on local folder + _base_path = Path(__file__).parent.absolute() + # linux do put 'lib' as name prefix and can put the version after the extension (e.g. libdmtx.so.0.1.0 ) + _lib_search = [f for f in os.listdir(_base_path) if re.search(rf'[a-z]*{_lib_name}[a_z]*.so[.0-9]*', f)] + if _lib_search: + path = _base_path.joinpath(_lib_search[0]) + if not path: raise ImportError('Unable to find dmtx shared library') libdmtx = cdll.LoadLibrary(path) diff --git a/pylibdmtx/pylibdmtx.py b/pylibdmtx/pylibdmtx.py index 3b02e4a..9803e38 100644 --- a/pylibdmtx/pylibdmtx.py +++ b/pylibdmtx/pylibdmtx.py @@ -12,13 +12,13 @@ dmtxDecodeDestroy, dmtxRegionDestroy, dmtxMessageDestroy, dmtxTimeAdd, dmtxTimeNow, dmtxDecodeMatrixRegion, dmtxRegionFindNext, dmtxMatrix3VMultiplyBy, dmtxDecodeSetProp, DmtxPackOrder, DmtxProperty, - DmtxUndefined, DmtxVector2, EXTERNAL_DEPENDENCIES, + DmtxUndefined, DmtxVector2, EXTERNAL_DEPENDENCIES, DmtxFalse, DmtxTrue, DmtxSymbolSize, DmtxScheme, dmtxEncodeSetProp, dmtxEncodeDataMatrix, - dmtxImageGetProp, dmtxEncodeCreate, dmtxEncodeDestroy + dmtxImageGetProp, dmtxEncodeCreate, dmtxEncodeDestroy, dmtxHasReaderProgramming ) __all__ = [ - 'decode', 'encode', 'Encoded', 'ENCODING_SCHEME_NAMES', + 'decode', 'encode', 'Encoded', 'ENCODING_SCHEME_NAMES', 'DmtxFalse', 'DmtxTrue', 'ENCODING_SIZE_NAMES', 'EXTERNAL_DEPENDENCIES', ] @@ -313,7 +313,7 @@ def _encoder(): dmtxEncodeDestroy(byref(encoder)) -def encode(data, scheme=None, size=None): +def encode(data, scheme=None, size=None, reader_programming=DmtxFalse): """ Encodes `data` in a DataMatrix image. @@ -360,7 +360,15 @@ def encode(data, scheme=None, size=None): dmtxEncodeSetProp(encoder, DmtxProperty.DmtxPropScheme, scheme) dmtxEncodeSetProp(encoder, DmtxProperty.DmtxPropSizeRequest, size) - if dmtxEncodeDataMatrix(encoder, len(data), cast(data, c_ubyte_p)) == 0: + if reader_programming == DmtxTrue and dmtxHasReaderProgramming(): + dmtxEncodeSetProp(encoder, DmtxProperty.DmtxPropFnc1, 0xF1) + + if dmtxHasReaderProgramming(): + _ret = dmtxEncodeDataMatrix(encoder, len(data), cast(data, c_ubyte_p), reader_programming) + else: + _ret = dmtxEncodeDataMatrix(encoder, len(data), cast(data, c_ubyte_p)) + + if _ret == 0: raise PyLibDMTXError( 'Could not encode data, possibly because the image is not ' 'large enough to contain the data' diff --git a/pylibdmtx/scripts/write_datamatrix.py b/pylibdmtx/scripts/write_datamatrix.py index 9522e17..15bc814 100644 --- a/pylibdmtx/scripts/write_datamatrix.py +++ b/pylibdmtx/scripts/write_datamatrix.py @@ -6,7 +6,7 @@ import pylibdmtx from pylibdmtx.pylibdmtx import ( - encode, ENCODING_SIZE_NAMES, ENCODING_SCHEME_NAMES + encode, ENCODING_SIZE_NAMES, ENCODING_SCHEME_NAMES, DmtxFalse, DmtxTrue ) @@ -31,6 +31,9 @@ def main(args=None): help="Encoding method; default is 'Ascii'", choices=ENCODING_SCHEME_NAMES ) + parser.add_argument( + '-r', '--reader-programming', action='store_true' + ) parser.add_argument( '-v', '--version', action='version', version='%(prog)s ' + pylibdmtx.__version__ @@ -39,8 +42,12 @@ def main(args=None): from PIL import Image + _rp = DmtxFalse + if args.reader_programming: + _rp = DmtxTrue + encoded = encode( - args.data.encode('utf-8'), size=args.size, scheme=args.scheme + args.data.encode('utf-8'), size=args.size, scheme=args.scheme, reader_programming=_rp ) im = Image.frombytes('RGB', (encoded.width, encoded.height), encoded.pixels) im.save(args.file) diff --git a/pylibdmtx/tests/reader_programming.bmp b/pylibdmtx/tests/reader_programming.bmp new file mode 100644 index 0000000..c618cc8 Binary files /dev/null and b/pylibdmtx/tests/reader_programming.bmp differ diff --git a/pylibdmtx/tests/test_reader_programming.py b/pylibdmtx/tests/test_reader_programming.py new file mode 100644 index 0000000..92bdda2 --- /dev/null +++ b/pylibdmtx/tests/test_reader_programming.py @@ -0,0 +1,84 @@ +import os +import sys +import tempfile +import unittest + +from PIL import Image +from PIL import ImageChops +from pathlib import Path +from contextlib import contextmanager + +# TODO Would io.StringIO not work in all cases? +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + +from pylibdmtx.scripts.read_datamatrix import main as main_read +from pylibdmtx.scripts.write_datamatrix import main as main_write +from pylibdmtx.wrapper import dmtxVersion, dmtxHasReaderProgramming + + +@contextmanager +def capture_stdout(): + sys.stdout, old_stdout = StringIO(), sys.stdout + try: + yield sys.stdout + finally: + sys.stdout = old_stdout + + +class TestReaderProgramming(unittest.TestCase): + + def test_libdmtx_version(self): + """Check feature availability on minimum version and if it was compiled.""" + self.assertGreaterEqual(dmtxVersion(), '1.0.0', 'Feature not present on older library version.') + self.assertEqual(dmtxHasReaderProgramming(), True, 'Feature not built on loaded library.') + + def test_read_datamatrix_reader_programming(self): + """Read datamatrix reader programming barcodes.""" + with capture_stdout() as stdout: + main_read([str(Path(__file__).parent.joinpath('reader_programming.bmp'))]) + + if 2 == sys.version_info[0]: + expected = "$P\\r" + else: + expected = "b'$P\\r'" + + self.assertEqual(expected, stdout.getvalue().strip()) + + def test_write_reader_programming_datamatrix(self): + """Create reader programming datamatrix and read it.""" + tmpfile = tempfile.NamedTemporaryFile(suffix='.png', delete=False) + tmpfile.close() + try: + main_write(['--reader-programming', '--size', '16x16', tmpfile.name, '$P\r']) + with capture_stdout() as stdout: + main_read([tmpfile.name]) + + expected = ( + "$P\\r" if 2 == sys.version_info[0] else "b'$P\\r'" + ) + self.assertEqual(expected, stdout.getvalue().strip()) + finally: + os.unlink(tmpfile.name) + + def test_compare_reader_programming_images(self): + """Compare output image with existing correct one.""" + tmpfile = tempfile.NamedTemporaryFile(suffix='.bmp', delete=False) + tmpfile.close() + try: + main_write(['--reader-programming', '--size', '16x16', tmpfile.name, '$P\r']) + + _expected_image = Image.open(Path(__file__).parent.joinpath('reader_programming.bmp')) + _created_image = Image.open(tmpfile.name) + + diff = ImageChops.difference(_expected_image, _created_image) + + self.assertEqual(diff.getbbox(), None) + finally: + os.unlink(tmpfile.name) + + +if __name__ == '__main__': + unittest.main() diff --git a/pylibdmtx/wrapper.py b/pylibdmtx/wrapper.py index 47edc30..f150281 100644 --- a/pylibdmtx/wrapper.py +++ b/pylibdmtx/wrapper.py @@ -16,7 +16,8 @@ 'dmtxDecodeCreate', 'dmtxDecodeDestroy', 'dmtxRegionDestroy', 'dmtxMessageDestroy', 'dmtxTimeAdd', 'dmtxMatrix3VMultiplyBy', 'dmtxDecodeSetProp', 'DmtxPackOrder', 'DmtxProperty', 'dmtxTimeNow', - 'dmtxDecodeMatrixRegion', 'dmtxRegionFindNext' + 'dmtxDecodeMatrixRegion', 'dmtxRegionFindNext', 'dmtxVersion', + 'dmtxHasReaderProgramming' ] # Globals populated in load_libdmtx @@ -29,6 +30,8 @@ """ +_reader_programming_min_version = '1.0.0' + def load_libdmtx(): """Loads the libdmtx shared library. @@ -66,6 +69,9 @@ def libdmtx_function(fname, restype, *args): # Defines and enums DmtxUndefined = -1 +DmtxTrue = 1 +DmtxFalse = 0 + # Define this function early so that it can be used in the definitions below. _dmtxVersion = libdmtx_function('dmtxVersion', c_char_p) @@ -80,6 +86,27 @@ def dmtxVersion(): return _dmtxVersion().decode() +if LooseVersion(dmtxVersion()) < LooseVersion(_reader_programming_min_version): + def dmtxHasReaderProgramming() -> bool: + """Returns False, feature not present on older verion. + + Returns: + bool: Feature not enabled + """ + return False +else: + _dmtxHasReaderProgramming = libdmtx_function('dmtxHasReaderProgramming', c_uint) + + def dmtxHasReaderProgramming() -> bool: + """Returns true if Reader Programming feature was configured at build time. + + Returns: + bool: Feature enabled or not + """ + _ret_val = True if _dmtxHasReaderProgramming() == 1 else False + return _ret_val + + @unique class DmtxProperty(IntEnum): DmtxPropScheme = 100 @@ -193,6 +220,7 @@ class DmtxSymbolSize(IntEnum): # Types DmtxPassFail = c_uint DmtxMatrix3 = c_double * 3 * 3 +DmtxBoolean = c_uint # Structs @@ -543,10 +571,22 @@ class DmtxEncode(Structure): c_int # value ) -dmtxEncodeDataMatrix = libdmtx_function( - 'dmtxEncodeDataMatrix', - DmtxPassFail, - POINTER(DmtxEncode), - c_int, - POINTER(c_ubyte) -) +if dmtxHasReaderProgramming(): + dmtxEncodeDataMatrix = libdmtx_function( + 'dmtxEncodeDataMatrix', + DmtxPassFail, + POINTER(DmtxEncode), + c_int, + POINTER(c_ubyte), + DmtxBoolean + ) +else: + dmtxEncodeDataMatrix = libdmtx_function( + 'dmtxEncodeDataMatrix', + DmtxPassFail, + POINTER(DmtxEncode), + c_int, + POINTER(c_ubyte) + ) + +