Skip to content

Commit

Permalink
[#19] Improve error messages. Check consitency of image dimensions.
Browse files Browse the repository at this point in the history
  • Loading branch information
quicklizard99 committed Mar 19, 2018
1 parent 29a53cf commit 72f7d73
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### v0.1.6

* #19 Improve error messages

### v0.1.5

* #11 Barcode locations
Expand Down
78 changes: 50 additions & 28 deletions pyzbar/pyzbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@
Decoded = namedtuple('Decoded', ['data', 'type', 'rect'])

# ZBar's magic 'fourcc' numbers that represent image formats
FOURCC = {
_FOURCC = {
'L800': 808466521,
'GRAY': 1497715271
}

RANGEFN = getattr(globals(), 'xrange', range)
_RANGEFN = getattr(globals(), 'xrange', range)


@contextmanager
def zbar_image():
def _image():
"""A context manager for `zbar_image`, created and destoyed by
`zbar_image_create` and `zbar_image_destroy`.
Expand All @@ -47,7 +47,7 @@ def zbar_image():
"""
image = zbar_image_create()
if not image:
raise PyZbarError('Could not create image')
raise PyZbarError('Could not create zbar image')
else:
try:
yield image
Expand All @@ -56,7 +56,7 @@ def zbar_image():


@contextmanager
def zbar_image_scanner():
def _image_scanner():
"""A context manager for `zbar_image_scanner`, created and destroyed by
`zbar_image_scanner_create` and `zbar_image_scanner_destroy`.
Expand All @@ -68,15 +68,15 @@ def zbar_image_scanner():
"""
scanner = zbar_image_scanner_create()
if not scanner:
raise PyZbarError('Could not create decoder')
raise PyZbarError('Could not create image scanner')
else:
try:
yield scanner
finally:
zbar_image_scanner_destroy(scanner)


def bounding_box_of_locations(locations):
def _bounding_box_of_locations(locations):
"""Computes a bounding box from scan locations.
Args:
Expand All @@ -93,7 +93,7 @@ def bounding_box_of_locations(locations):
return Rect(x_min, y_min, x_max - x_min, y_max - y_min)


def symbols_for_image(image):
def _symbols_for_image(image):
"""Generator of symbols.
Args:
Expand All @@ -108,7 +108,7 @@ def symbols_for_image(image):
symbol = zbar_symbol_next(symbol)


def decode_symbols(symbols):
def _decode_symbols(symbols):
"""Generator of decoded symbol information.
Args:
Expand All @@ -126,30 +126,22 @@ def decode_symbols(symbols):
zbar_symbol_get_loc_x(symbol, index),
zbar_symbol_get_loc_y(symbol, index)
)
for index in RANGEFN(zbar_symbol_get_loc_size(symbol))
for index in _RANGEFN(zbar_symbol_get_loc_size(symbol))
]

yield Decoded(
data=data,
type=symbol_type,
rect=bounding_box_of_locations(locations),
rect=_bounding_box_of_locations(locations),
)


def decode(image, symbols=None, scan_locations=False):
"""Decodes datamatrix barcodes in `image`.
Args:
image: `numpy.ndarray`, `PIL.Image` or tuple (pixels, width, height)
symbols (ZBarSymbol): the symbol types to decode; if `None`, uses
`zbar`'s default behaviour, which is to decode all symbol types.
scan_locations (bool): If `True`, results will include scan
locations.
def _pixel_data(image):
"""Returns (pixels, width, height)
Returns:
:obj:`list` of :obj:`Decoded`: The values decoded from barcodes.
:obj: `tuple` (pixels, width, height)
"""

# Test for PIL.Image and numpy.ndarray without requiring that cv2 or PIL
# are installed.
if 'PIL.' in str(type(image)):
Expand All @@ -174,13 +166,43 @@ def decode(image, symbols=None, scan_locations=False):
# image should be a tuple (pixels, width, height)
pixels, width, height = image

# Check dimensions
if 0 != len(pixels) % (width * height):
raise PyZbarError((
'Inconsistent dimensions: image data of {0} bytes is not '
'divisible by (width x height = {1})'
).format(len(pixels), (width * height))
)

# Compute bits-per-pixel
bpp = 8 * len(pixels) / (width * height)
bpp = 8 * len(pixels) // (width * height)
if 8 != bpp:
raise PyZbarError('Unsupported bits-per-pixel [{0}]'.format(bpp))
raise PyZbarError(
'Unsupported bits-per-pixel [{0}]. Only [8] is supported.'.format(
bpp
)
)

return pixels, width, height


def decode(image, symbols=None, scan_locations=False):
"""Decodes datamatrix barcodes in `image`.
Args:
image: `numpy.ndarray`, `PIL.Image` or tuple (pixels, width, height)
symbols (ZBarSymbol): the symbol types to decode; if `None`, uses
`zbar`'s default behaviour, which is to decode all symbol types.
scan_locations (bool): If `True`, results will include scan
locations.
Returns:
:obj:`list` of :obj:`Decoded`: The values decoded from barcodes.
"""
pixels, width, height = _pixel_data(image)

results = []
with zbar_image_scanner() as scanner:
with _image_scanner() as scanner:
if symbols:
# Disable all but the symbols of interest
disable = set(ZBarSymbol).difference(symbols)
Expand All @@ -196,14 +218,14 @@ def decode(image, symbols=None, scan_locations=False):
zbar_image_scanner_set_config(
scanner, symbol, ZBarConfig.CFG_ENABLE, 1
)
with zbar_image() as img:
zbar_image_set_format(img, FOURCC['L800'])
with _image() as img:
zbar_image_set_format(img, _FOURCC['L800'])
zbar_image_set_size(img, width, height)
zbar_image_set_data(img, cast(pixels, c_void_p), len(pixels), None)
decoded = zbar_scan_image(scanner, img)
if decoded < 0:
raise PyZbarError('Unsupported image format')
else:
results.extend(decode_symbols(symbols_for_image(img)))
results.extend(_decode_symbols(_symbols_for_image(img)))

return results
43 changes: 34 additions & 9 deletions pyzbar/tests/test_pyzbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path

try:
from unittest.mock import call, patch
from unittest.mock import call, patch, MagicMock
except ImportError:
# Python 2
from mock import call, patch
Expand Down Expand Up @@ -95,8 +95,7 @@ def test_unsupported_bpp(self):
def test_empty(self):
"Do not show any output for an image that does not contain a barcode"
res = decode(self.empty)
expected = []
self.assertEqual(expected, res)
self.assertEqual([], res)

def test_decode_numpy(self):
"Read image using Pillow and convert to numpy.ndarray"
Expand All @@ -106,9 +105,7 @@ def test_decode_numpy(self):
@unittest.skipIf(cv2 is None, 'OpenCV not installed')
def test_decode_opencv(self):
"Read image using OpenCV"
res = decode(
cv2.imread(str(TESTDATA.joinpath('code128.png')))
)
res = decode(cv2.imread(str(TESTDATA.joinpath('code128.png'))))
self.assertEqual(self.EXPECTED_CODE128, res)

def test_external_dependencies(self):
Expand All @@ -125,21 +122,49 @@ def test_external_dependencies(self):
@patch('pyzbar.pyzbar.zbar_image_create')
def test_zbar_image_create_fail(self, zbar_image_create):
zbar_image_create.return_value = None
self.assertRaises(PyZbarError, decode, self.code128)
self.assertRaisesRegexp(
PyZbarError, 'Could not create zbar image', decode, self.code128
)
zbar_image_create.assert_called_once_with()

@patch('pyzbar.pyzbar.zbar_image_scanner_create')
def test_zbar_image_scanner_create_fail(self, zbar_image_scanner_create):
zbar_image_scanner_create.return_value = None
self.assertRaises(PyZbarError, decode, self.code128)
self.assertRaisesRegexp(
PyZbarError, 'Could not create image scanner', decode, self.code128
)
zbar_image_scanner_create.assert_called_once_with()

@patch('pyzbar.pyzbar.zbar_scan_image')
def test_zbar_scan_image_fail(self, zbar_scan_image):
zbar_scan_image.return_value = -1
self.assertRaises(PyZbarError, decode, self.code128)
self.assertRaisesRegexp(
PyZbarError, 'Unsupported image format', decode, self.code128
)
self.assertEqual(1, zbar_scan_image.call_count)

def test_unsupported_bits_per_pixel(self):
# 16 bits-per-pixel
data = (list(range(3 * 3 * 2)), 3, 3)
self.assertRaisesRegexp(
PyZbarError,
'Unsupported bits-per-pixel \[16\]. Only \[8\] is supported.',
decode, data
)
self.assertRaises(PyZbarError, decode, data)

def test_inconsistent_dimensions(self):
# Ten bytes but width x height indicates nine bytes
data = (list(range(10)), 3, 3)
self.assertRaisesRegexp(
PyZbarError,
(
'Inconsistent dimensions: image data of 10 bytes is not '
'divisible by \(width x height = 9\)'
),
decode, data
)


if __name__ == '__main__':
unittest.main()

0 comments on commit 72f7d73

Please sign in to comment.