yubioath-flutter/py/qr/qrdecode.py

317 lines
12 KiB
Python
Raw Normal View History

2017-02-15 13:17:26 +03:00
"""
Given a 2D matrix of pixel data from a QR code, this module will decode and
return the data contained within. Note that error correction is not
implemented, and the input will thus have to be without any errors. Only
supports numeric, alphanumeric and byte encodings.
"""
from __future__ import division
def decode_qr_data(qr_data):
"""Given a 2D matrix of QR data, returns the encoded string"""
size = len(qr_data)
version = (size - 17) // 4
level = bits_to_int(qr_data[8][:2])
mask = bits_to_int(qr_data[8][2:5]) ^ 0b101
read_mask = [x[:] for x in [[1]*size]*size]
# Verify/Remove alignment patterns
remove_locator_patterns(qr_data, read_mask)
remove_alignment_patterns(read_mask, version)
remove_timing_patterns(read_mask)
if version >= 7: # QR Codes version 7 or larger have version info.
remove_version_info(read_mask)
# Read and deinterleave
buf = bits_to_bytes(read_bits(qr_data, read_mask, mask))
buf = deinterleave(buf, INTERLEAVE_PARAMS[version][level])
bits = bytes_to_bits(buf)
# Decode data
buf = ''
while bits:
data, bits = parse_bits(bits, version)
buf += data
return buf
LOCATOR_BOX = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1]
]
MASKS = [
lambda x, y: (y+x) % 2 == 0,
lambda x, y: y % 2 == 0,
lambda x, y: x % 3 == 0,
lambda x, y: (y+x) % 3 == 0,
lambda x, y: (y//2 + x//3) % 2 == 0,
lambda x, y: (y*x) % 2 + (y*x) % 3 == 0,
lambda x, y: ((y*x) % 2 + (y*x) % 3) % 2 == 0,
lambda x, y: ((y+x) % 2 + (y*x) % 3) % 2 == 0
]
ALPHANUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
EC_LEVELS = ['H', 'Q', 'M', 'L']
INTERLEAVE_PARAMS = [ # From ISO/IEC 18004:2006
[], # H, Q, M, L
[1 * (9,), 1 * (13,), 1 * (16,), 1 * (19,)], # Version 1
[1 * (16,), 1 * (22,), 1 * (28,), 1 * (34,)],
[2 * (13,), 2 * (17,), 1 * (44,), 1 * (55,)],
[4 * (9,), 2 * (24,), 2 * (32,), 1 * (80,)],
[2 * (11,) + 2 * (12,), 2 * (15,) + 2 * (16,), 2 * (43,), 1 * (108,)],
[4 * (15,), 4 * (19,), 4 * (27,), 2 * (68,)],
[4 * (13,) + 1 * (14,), 2 * (14,) + 4 * (15,), 4 * (31,), 2 * (78,)],
[4 * (14,) + 2 * (15,), 4 * (18,) + 2 * (19,), 2 * (38,) + 2 * (39,), 2 * (97,)], # noqa: E501
[4 * (12,) + 4 * (13,), 4 * (16,) + 4 * (17,), 3 * (36,) + 2 * (37,), 2 * (116,)], # noqa: E501
[6 * (15,) + 2 * (16,), 6 * (19,) + 2 * (20,), 4 * (43,) + 1 * (44,), 2 * (68,) + 2 * (69,)], # noqa: E501
[3 * (12,) + 8 * (13,), 4 * (22,) + 4 * (23,), 1 * (50,) + 4 * (51,), 4 * (81,)], # noqa: E501
[7 * (14,) + 4 * (15,), 4 * (20,) + 6 * (21,), 6 * (36,) + 2 * (37,), 2 * (92,) + 2 * (93,)], # noqa: E501
[12 * (11,) + 4 * (12,), 8 * (20,) + 4 * (21,), 8 * (37,) + 1 * (38,), 4 * (107,)], # noqa: E501
[11 * (12,) + 5 * (13,), 11 * (16,) + 5 * (17,), 4 * (40,) + 5 * (41,), 3 * (115,) + 1 * (116,)], # noqa: E501
[11 * (12,) + 7 * (13,), 5 * (24,) + 7 * (25,), 5 * (41,) + 5 * (42,), 5 * (87,) + 1 * (88,)], # noqa: E501
[3 * (15,) + 13 * (16,), 15 * (19,) + 2 * (20,), 7 * (45,) + 3 * (46,), 5 * (98,) + 1 * (99,)], # noqa: E501
[2 * (14,) + 17 * (15,), 1 * (22,) + 15 * (23,), 10 * (46,) + 1 * (47,), 1 * (107,) + 5 * (108,)], # noqa: E501
[2 * (14,) + 19 * (15,), 17 * (22,) + 1 * (23,), 9 * (43,) + 4 * (44,), 5 * (120,) + 1 * (121,)], # noqa: E501
[9 * (13,) + 16 * (14,), 17 * (21,) + 4 * (22,), 3 * (44,) + 11 * (45,), 3 * (113,) + 4 * (114,)], # noqa: E501
[15 * (15,) + 10 * (16,), 15 * (24,) + 5 * (25,), 3 * (41,) + 13 * (42,), 3 * (107,) + 5 * (108,)], # noqa: E501
[19 * (16,) + 6 * (17,), 17 * (22,) + 6 * (23,), 17 * (42,), 4 * (116,) + 4 * (117,)], # noqa: E501
[34 * (13,), 7 * (24,) + 16 * (25,), 17 * (46,), 2 * (111,) + 7 * (112,)],
[16 * (15,) + 14 * (16,), 11 * (24,) + 14 * (25,), 4 * (47,) + 14 * (48,), 4 * (121,) + 5 * (122,)], # noqa: E501
[30 * (16,) + 2 * (17,), 11 * (24,) + 16 * (25,), 6 * (45,) + 14 * (46,), 6 * (117,) + 4 * (118,)], # noqa: E501
[22 * (15,) + 13 * (16,), 7 * (24,) + 22 * (25,), 8 * (47,) + 13 * (48,), 8 * (106,) + 4 * (107,)], # noqa: E501
[33 * (16,) + 4 * (17,), 28 * (22,) + 6 * (23,), 19 * (46,) + 4 * (47,), 10 * (114,) + 2 * (115,)], # noqa: E501
[12 * (15,) + 28 * (16,), 8 * (23,) + 26 * (24,), 22 * (45,) + 3 * (46,), 8 * (122,) + 4 * (123,)], # noqa: E501
[11 * (15,) + 31 * (16,), 4 * (24,) + 31 * (25,), 3 * (45,) + 23 * (46,), 3 * (117,) + 10 * (118,)], # noqa: E501
[19 * (15,) + 26 * (16,), 1 * (23,) + 37 * (24,), 21 * (45,) + 7 * (46,), 7 * (116,) + 7 * (117,)], # noqa: E501
[23 * (15,) + 25 * (16,), 15 * (24,) + 25 * (25,), 19 * (47,) + 10 * (48,), 5 * (115,) + 10 * (116,)], # noqa: E501
[23 * (15,) + 28 * (16,), 42 * (24,) + 1 * (25,), 2 * (46,) + 29 * (47,), 13 * (115,) + 3 * (116,)], # noqa: E501
[19 * (15,) + 35 * (16,), 10 * (24,) + 35 * (25,), 10 * (46,) + 23 * (47,), 17 * (115,)], # noqa: E501
[11 * (15,) + 46 * (16,), 29 * (24,) + 19 * (25,), 14 * (46,) + 21 * (47,), 17 * (115,) + 1 * (116,)], # noqa: E501
[59 * (16,) + 1 * (17,), 44 * (24,) + 7 * (25,), 14 * (46,) + 23 * (47,), 13 * (115,) + 6 * (116,)], # noqa: E501
[22 * (15,) + 41 * (16,), 39 * (24,) + 14 * (25,), 12 * (47,) + 26 * (48,), 12 * (121,) + 7 * (122,)], # noqa: E501
[2 * (15,) + 64 * (16,), 46 * (24,) + 10 * (25,), 6 * (47,) + 34 * (48,), 6 * (121,) + 14 * (122,)], # noqa: E501
[24 * (15,) + 46 * (16,), 49 * (24,) + 10 * (25,), 29 * (46,) + 14 * (47,), 17 * (122,) + 4 * (123,)], # noqa: E501
[42 * (15,) + 32 * (16,), 48 * (24,) + 14 * (25,), 13 * (46,) + 32 * (47,), 4 * (122,) + 18 * (123,)], # noqa: E501
[10 * (15,) + 67 * (16,), 43 * (24,) + 22 * (25,), 40 * (47,) + 7 * (48,), 20 * (117,) + 4 * (118,)], # noqa: E501
[20 * (15,) + 61 * (16,), 34 * (24,) + 34 * (25,), 18 * (47,) + 31 * (48,), 19 * (118,) + 6 * (119,)], # noqa: E501
]
ALIGNMENT_POSITIONS = [ # From ISO/IEC 18004:2006
[],
[],
[18], # Version 2
[22],
[26],
[30],
[34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106], # Version 24
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
]
def check_region(data, x, y, match):
"""Compares a region to the given """
w = len(match[0])
for cy in range(len(match)):
if match[cy] != data[y+cy][x:x+w]:
return False
return True
def zero_region(data, x, y, w, h):
"""Fills a region with zeroes."""
for by in range(y, y+h):
line = data[by]
data[by] = line[:x] + [0]*w + line[x+w:]
def bits_to_int(bits):
"""Convers a list of bits into an integer"""
val = 0
for bit in bits:
val = (val << 1) | bit
return val
def bits_to_bytes(bits):
"""Converts a list of bits into a string of bytes"""
return ''.join([chr(bits_to_int(bits[i:i+8]))
for i in range(0, len(bits), 8)])
def bytes_to_bits(buf):
"""Converts a string of bytes to a list of bits"""
return [b >> i & 1 for b in map(ord, buf) for i in range(7, -1, -1)]
def deinterleave(data, b_cap):
"""De-interleaves the bytes from a QR code"""
n_bufs = len(b_cap)
bufs = []
for _ in range(n_bufs):
bufs.append([])
b_i = 0
for i in range(sum(b_cap)):
b = data[i]
while b_cap[b_i] <= len(bufs[b_i]):
b_i = (b_i + 1) % n_bufs
bufs[b_i].append(b)
b_i = (b_i + 1) % n_bufs
buf = ''
for b in bufs:
buf += ''.join(b)
return buf
def parse_bits(bits, version):
"""
Parses and decodes a TLV value from the given list of bits.
Returns the parsed data and the remaining bits, if any.
"""
enc, bits = bits_to_int(bits[:4]), bits[4:]
if enc == 0: # End of data.
return '', []
elif enc == 1: # Number
n_l = 10 if version < 10 else 12 if version < 27 else 14
2017-11-07 16:56:33 +03:00
ln, bits = bits_to_int(bits[:n_l]), bits[n_l:]
2017-02-15 13:17:26 +03:00
buf = ''
2017-11-07 16:56:33 +03:00
while ln > 0:
if ln >= 3:
2017-02-15 13:17:26 +03:00
num, bits = bits_to_int(bits[:10]), bits[10:]
2017-11-07 16:56:33 +03:00
ln -= 3
elif ln >= 2:
2017-02-15 13:17:26 +03:00
num, bits = bits_to_int(bits[:7]), bits[7:]
2017-11-07 16:56:33 +03:00
ln -= 2
2017-02-15 13:17:26 +03:00
else:
num, bits = bits_to_int(bits[:3]), bits[3:]
2017-11-07 16:56:33 +03:00
ln -= 1
2017-02-15 13:17:26 +03:00
buf += str(num)
return buf, bits
2017-02-15 13:17:26 +03:00
elif enc == 2: # Alphanumeric
n_l = 9 if version < 10 else 11 if version < 27 else 13
2017-11-07 16:56:33 +03:00
ln, bits = bits_to_int(bits[:n_l]), bits[n_l:]
2017-02-15 13:17:26 +03:00
buf = ''
2017-11-07 16:56:33 +03:00
while ln > 0:
if ln >= 2:
2017-02-15 13:17:26 +03:00
num, bits = bits_to_int(bits[:11]), bits[11:]
buf += ALPHANUM[num // 45]
buf += ALPHANUM[num % 45]
2017-11-07 16:56:33 +03:00
ln -= 2
2017-02-15 13:17:26 +03:00
else:
num, bits = bits_to_int(bits[:6]), bits[6:]
buf += ALPHANUM[num]
2017-11-07 16:56:33 +03:00
ln -= 1
2017-02-15 13:17:26 +03:00
return buf, bits
elif enc == 4: # Bytes
n_l = 8 if version < 10 else 16
2017-11-07 16:56:33 +03:00
ln, bits = bits_to_int(bits[:n_l]), bits[n_l:]
return bits_to_bytes(bits[:ln*8]), bits[ln*8:]
2017-02-15 13:17:26 +03:00
else:
raise ValueError('Unsupported encoding: %d' % enc)
def remove_locator_patterns(data, mask):
"""
Verifies and blanks out the three large locator patterns and dedicated
whitespace surrounding them.
"""
width = len(data)
if not check_region(data, 0, 0, LOCATOR_BOX):
raise ValueError('Top-left square missing')
zero_region(mask, 0, 0, 9, 9)
if not check_region(data, width-7, 0, LOCATOR_BOX):
raise ValueError('Top-right square missing')
zero_region(mask, width-8, 0, 8, 9)
if not check_region(data, 0, width-7, LOCATOR_BOX):
raise ValueError('Bottom-left square missing')
zero_region(mask, 0, width-8, 9, 8)
def remove_alignment_patterns(mask, version):
"""Blanks out alignment patterns."""
positions = ALIGNMENT_POSITIONS[version]
for y in positions:
for x in positions:
# Do not try to remove patterns in locator pattern positions.
if (x, y) not in [(6, 6), (6, positions[-1]), (positions[-1], 6)]:
zero_region(mask, x-2, y-2, 5, 5)
def remove_timing_patterns(mask):
"""Blanks out tracking patterns."""
width = len(mask)
mask[6] = [0] * width
for y in range(width):
mask[y][6] = 0
def remove_version_info(mask):
"""Removes version data. Only for version 7 and greater."""
width = len(mask)
zero_region(mask, width-11, 0, 3, 6)
zero_region(mask, 0, width-11, 5, 6)
def read_bits(qr_data, read_mask, mask):
"""Reads the data contained in a QR code as bits."""
size = len(qr_data)
mask_f = MASKS[mask]
bits = []
# Skip over vertical timing pattern
for x in reversed(list(range(0, 6, 2)) + list(range(7, size, 2))):
y_range = range(0, size)
if (size - x) % 4 != 0:
y_range = reversed(y_range)
for y in y_range:
for i in reversed(range(2)):
if read_mask[y][x+i]:
bits.append(qr_data[y][x+i] ^ mask_f(x+i, y))
return bits