Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/enhancement for some failed QR decoding - Interleaving, size calculation, import error, mode zero #13

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ coverage.xml
# Sphinx documentation
docs/_build/

.idea/
.idea/
.venv/
7 changes: 4 additions & 3 deletions qreader/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
__all__ = ['read']


def read(image_or_path):
def read(image_or_path, raw_in_bytes_mode=False):
"""
Accepts either a path to a file, a PIL image, or a file-like object and reads a QR code data from it.
:param str|PIL.Image.Image|file|BufferedIOBase image_or_path: The source containing the QR code.
Expand All @@ -23,8 +23,9 @@ def read(image_or_path):
image_or_path = PIL.Image.open(image_or_path)
if isinstance(image_or_path, PIL.Image.Image):
data = ImageScanner(image_or_path)
return QRDecoder(data).get_first()
# result = QRDecoder(data).get_all()
decoder = QRDecoder(data, raw_in_bytes_mode=raw_in_bytes_mode)
return decoder.get_first()
# result = decoder.get_all()
# if len(result) == 0:
# return None
# elif len(result) == 1:
Expand Down
1 change: 1 addition & 0 deletions qreader/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ERROR_CORRECT_H = 3

# QR encoding modes (based on qrcode package)
MODE_ZERO = 0
MODE_NUMBER = 1
MODE_ALPHA_NUM = 2
MODE_BYTES = 4
Expand Down
19 changes: 16 additions & 3 deletions qreader/decoder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from qreader.constants import MODE_NUMBER, MODE_ALPHA_NUM, ALPHANUM_CHARS, MODE_BYTES, MODE_KANJI, MODE_ECI, \
MODE_STRUCTURED_APPEND
MODE_STRUCTURED_APPEND, MODE_ZERO
from qreader.exceptions import IllegalQrMessageModeId
from qreader.spec import bits_for_length
from qreader.utils import ints_to_bytes
Expand All @@ -10,8 +10,9 @@

class QRDecoder(object):

def __init__(self, scanner):
def __init__(self, scanner, raw_in_bytes_mode=False):
self.scanner = scanner
self._raw_in_bytes_mode = raw_in_bytes_mode

@property
def version(self):
Expand All @@ -21,7 +22,14 @@ def get_first(self):
return self._decode_next_message()

def __iter__(self):
yield self._decode_next_message()
# yield self._decode_next_message()
return self

def __next__(self):
msg = self._decode_next_message()
if msg is None:
raise StopIteration()
return msg

def get_all(self):
return list(self)
Expand All @@ -31,6 +39,7 @@ def _decode_next_message(self):
return self._decode_message(mode)

def _decode_message(self, mode):
# print('Mode:', mode)
if mode == MODE_NUMBER:
message = self._decode_numeric_message()
elif mode == MODE_ALPHA_NUM:
Expand All @@ -43,6 +52,8 @@ def _decode_message(self, mode):
raise NotImplementedError('Structured append encoding not implemented yet')
elif mode == MODE_ECI:
raise NotImplementedError('Extended Channel Interpretation encoding not implemented yet')
elif mode == MODE_ZERO:
message = None
else:
raise IllegalQrMessageModeId(mode)
return message
Expand Down Expand Up @@ -74,6 +85,8 @@ def _decode_alpha_num_message(self):
def _decode_bytes_message(self):
char_count = self.scanner.read_int(bits_for_length(self.version, MODE_BYTES))
raw = ints_to_bytes(self.scanner.read_int(8) for _ in range(char_count))
if self._raw_in_bytes_mode:
return raw
try:
val = raw.decode('utf-8')
except UnicodeDecodeError:
Expand Down
123 changes: 105 additions & 18 deletions qreader/scanner.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from collections import Iterator
import math

from qreader.utils import print_bit_grid, save_bit_grid_as_pbm

try:
from collections import Iterator
except ImportError:
from collections.abc import Iterator

from qreader import tuples
from qreader.exceptions import QrImageRecognitionException
from qreader.spec import get_mask_func, FORMAT_INFO_MASK, get_dead_zones, ec_level_from_format_info_code
from qreader.spec import get_mask_func, FORMAT_INFO_MASK, get_dead_zones, ec_level_from_format_info_code, \
reassemble_raw_data_blocks
from qreader.validation import validate_format_info, validate_data

__author__ = 'ewino'
Expand All @@ -24,14 +32,19 @@ def info(self):
""" The meta info for the QR code. Reads the code on access if needed.
:rtype: QRCodeInfo
"""
if not self._was_read:
self.read()
# if not self._was_read:
# self.read()
self.read_info()
return self._info

def read(self):
self._was_read = True
self.read_info()
self.data = validate_data(self._read_all_data(), self.info.version, self.info.error_correction_level)

raw_bit_data = self._read_all_data()
reassembled_bit_data = reassemble_raw_data_blocks(raw_bit_data, self.info.version, self.info.error_correction_level)

self.data = validate_data(reassembled_bit_data, self.info.version, self.info.error_correction_level)
self._data_len = len(self.data)
self.reset()

Expand Down Expand Up @@ -87,16 +100,69 @@ def get_mask(self):
return {(x, y): 1 if mask_func(y, x) else 0 for x in range(self.info.size) for y in range(self.info.size)}

def read_info(self):
info = QRCodeInfo()
info.canvas = self.get_image_borders()
info.block_size = self.get_block_size(info.canvas[:2])
info.size = int((info.canvas[2] - (info.canvas[0]) + 1) / info.block_size[0])
info.version = (info.size - 17) // 4
self._info = info
self._read_format_info()
self.mask = self.get_mask()
return info

if not self._info:
info = QRCodeInfo()
info.canvas = self.get_image_borders()
info.block_size = self.get_block_size(info.canvas[:2])
info.size = math.ceil((info.canvas[2] - (info.canvas[0]) + 1) / info.block_size[0])
info.version = (info.size - 17) // 4
self._info = info
self._read_format_info()
self.mask = self.get_mask()

# print('QR Info: ', info)
# self._build_all_orig_bits_grid()
# print('Original bits from image:')
# print_bit_grid(self._all_orig_bits_grid, info.size)
# save_bit_grid_as_pbm(self._all_orig_bits_grid, info.size, filename='debug-orig.pbm')

# self._build_all_masked_bits_grid()
# print('Unmasked bits from image:')
# print_bit_grid(self._all_masked_bits_grid, info.size)
# save_bit_grid_as_pbm(self._all_masked_bits_grid, info.size, filename='debug-unmasked.pbm')

return self._info

def _build_all_orig_bits_grid(self):
"""
Added for debugging. Builds a grid of 0's and 1's from the image
"""
all_bits = []
for y in range(self.info.size):
row_bits = 0
for x in range(self.info.size):
xy_bit = self._get_pixel(tuples.add(self.info.canvas[:2], tuples.multiply((x, y), self.info.block_size)))
row_bits = (row_bits << 1) + xy_bit

all_bits.append(row_bits)

self._all_orig_bits_grid = all_bits

def _build_all_masked_bits_grid(self):
"""
Added for debugging - Builds a new grid of 0's and 1's by applying mask on
the grid of 0's and 1's built by _build_all_orig_bits_grid() method
"""
ignored_pos = {(x, y) for zone in get_dead_zones(self.info.version)
for x in range(zone[0], zone[2] + 1)
for y in range(zone[1], zone[3] + 1)}

all_bits_masked = []

# Apply mask on all original bits grid
for y in range(self.info.size):
masked_row_bits = 0
for x in range(self.info.size):
xy_bit = self._get_bit((x, y))
if (x, y) not in ignored_pos:
xy_bit ^= self.mask[(x, y)]
masked_row_bits = (masked_row_bits << 1) + xy_bit

all_bits_masked.append(masked_row_bits)

# TODO Later Apply format-info mask on format info bits
self._all_masked_bits_grid = all_bits_masked

def _get_pixel(self, coords):
try:
shade, alpha = self.image.getpixel(coords)
Expand All @@ -116,14 +182,35 @@ def get_corner_pixel(canvas_corner, vector, max_distance):
('left', 'right')[vector[0] == -1]))

max_dist = min(self.image.width, self.image.height)
self.image = self.image.crop((0, 0, max_dist, max_dist))

min_x, min_y = get_corner_pixel((0, 0), (1, 1), max_dist)
max_x, max_x_y = get_corner_pixel((self.image.width - 1, 0), (-1, 1), max_dist)
max_y_x, max_y = get_corner_pixel((0, self.image.height - 1), (1, -1), max_dist)

max_x, max_x_y = get_corner_pixel((max_dist - 1, 0), (-1, 1), max_dist)
if max_x_y != min_y:
raise QrImageRecognitionException('Top-left position pattern not aligned with the top-right one')

# Since max_x & min_y are confirmed by now, let's crop the image upto max_x, and from min_y
self.image = self.image.crop((0, min_y, max_x+1, max_dist))
min_y = max_x_y = 0
max_dist = min(self.image.width, self.image.height)
self.image = self.image.crop((0, 0, max_dist, max_dist))

max_y_x, max_y = get_corner_pixel((0, max_dist - 1), (1, -1), max_dist)
if max_y_x != min_x:
raise QrImageRecognitionException('Top-left position pattern not aligned with the bottom-left one')
return min_x, min_y, max_x, max_y

self.image = self.image.crop((min_x, min_y, max_x+1, max_y+1))

max_x -= min_x
min_x = 0
max_y -= min_y
min_y = 0

if (min_x, min_y, max_x, max_y) != (0, 0, self.image.width - 1, self.image.height - 1):
raise QrImageRecognitionException('Image recognition failed')

return 0, 0, self.image.width - 1, self.image.height - 1

def get_block_size(self, img_start):
"""
Expand Down
Loading