mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-27 14:23:18 +03:00
Add QR decode logic
This commit is contained in:
parent
9d6e248cae
commit
e8d2bbcf69
312
py/qr/qrdecode.py
Normal file
312
py/qr/qrdecode.py
Normal file
@ -0,0 +1,312 @@
|
||||
"""
|
||||
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
|
||||
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
|
||||
buf = ''
|
||||
while l > 0:
|
||||
if l >= 3:
|
||||
num, bits = bits_to_int(bits[:10]), bits[10:]
|
||||
elif l >= 2:
|
||||
num, bits = bits_to_int(bits[:7]), bits[7:]
|
||||
else:
|
||||
num, bits = bits_to_int(bits[:3]), bits[3:]
|
||||
buf += str(num)
|
||||
elif enc == 2: # Alphanumeric
|
||||
n_l = 9 if version < 10 else 11 if version < 27 else 13
|
||||
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
|
||||
buf = ''
|
||||
while l > 0:
|
||||
if l >= 2:
|
||||
num, bits = bits_to_int(bits[:11]), bits[11:]
|
||||
buf += ALPHANUM[num // 45]
|
||||
buf += ALPHANUM[num % 45]
|
||||
l -= 2
|
||||
else:
|
||||
num, bits = bits_to_int(bits[:6]), bits[6:]
|
||||
buf += ALPHANUM[num]
|
||||
l -= 1
|
||||
return buf, bits
|
||||
elif enc == 4: # Bytes
|
||||
n_l = 8 if version < 10 else 16
|
||||
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
|
||||
return bits_to_bytes(bits[:l*8]), bits[l*8:]
|
||||
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
|
@ -48,20 +48,20 @@ def check_line(pixels):
|
||||
yield i - width, width
|
||||
|
||||
|
||||
def check_row(line, bpp, x_offs, x_width):
|
||||
def check_row(line, x_offs, x_width):
|
||||
return check_line(line[x_offs:x_offs+x_width])
|
||||
|
||||
|
||||
def check_col(image, bpp, x, y_offs, y_height):
|
||||
def check_col(image, x, y_offs, y_height):
|
||||
return check_line(bytes([image.get_line(i)[x]
|
||||
for i in range(y_offs, y_offs + y_height)]))
|
||||
|
||||
|
||||
def read_line(line, bpp, x_offs, x_width):
|
||||
matching_dark = not is_dark(line[x_offs*bpp:(x_offs+1)*bpp])
|
||||
def read_line(line, x_offs, x_width):
|
||||
matching_dark = not is_dark(line[x_offs])
|
||||
matched = []
|
||||
for x in range(x_offs, x_offs + x_width):
|
||||
pixel = line[x*bpp:(x+1)*bpp]
|
||||
pixel = line[x]
|
||||
if is_dark(pixel): # Dark pixel
|
||||
if matching_dark:
|
||||
matched[-1] += 1
|
||||
@ -77,7 +77,7 @@ def read_line(line, bpp, x_offs, x_width):
|
||||
return matching_dark, matched
|
||||
|
||||
|
||||
def read_bits(image, bpp, img_x, img_y, img_w, img_h, size):
|
||||
def read_bits(image, img_x, img_y, img_w, img_h, size):
|
||||
qr_x_w = img_w / size
|
||||
qr_y_h = img_h / size
|
||||
qr_data = []
|
||||
@ -104,9 +104,6 @@ FINDER = [
|
||||
|
||||
|
||||
def parse_qr_codes(image, min_res=2):
|
||||
size = image.size()
|
||||
bpp = image.bytesPerLine() // size.width()
|
||||
|
||||
finders = locate_finders(image, min_res)
|
||||
|
||||
# Arrange finders into QR codes and extract data
|
||||
@ -117,27 +114,26 @@ def parse_qr_codes(image, min_res=2):
|
||||
height = bl.y + bl.h - min_y
|
||||
|
||||
# Determine resolution by reading timing pattern
|
||||
line = image.scanLine(min_y + int(6.5 / 7 * max(tl.h, tr.h)))
|
||||
_, line_data = read_line(line, bpp, min_x, width)
|
||||
line = image.get_line(min_y + int(6.5 / 7 * max(tl.h, tr.h)))
|
||||
_, line_data = read_line(line, min_x, width)
|
||||
size = len(line_data) + 12
|
||||
|
||||
# Read QR code data
|
||||
yield read_bits(image, bpp, min_x, min_y, width, height, size)
|
||||
yield read_bits(image, min_x, min_y, width, height, size)
|
||||
|
||||
|
||||
def locate_finders(image, min_res):
|
||||
bpp = 1
|
||||
finders = set()
|
||||
for y in range(0, image.height, min_res * 3):
|
||||
for (x, w) in check_row(image.get_line(y), bpp, 0, image.width):
|
||||
for (x, w) in check_row(image.get_line(y), 0, image.width):
|
||||
x_offs = x + w // 2
|
||||
y_offs = max(0, y - w)
|
||||
y_height = min(image.height - y_offs, 2 * w)
|
||||
match = next(check_col(image, bpp, x_offs, y_offs, y_height), None)
|
||||
match = next(check_col(image, x_offs, y_offs, y_height), None)
|
||||
if match:
|
||||
(pos, h) = match
|
||||
y2 = y_offs + pos
|
||||
if read_bits(image, bpp, x, y2, w, h, 7) == FINDER:
|
||||
if read_bits(image, x, y2, w, h, 7) == FINDER:
|
||||
finders.add(Box(x, y2, w, h))
|
||||
|
||||
return list(finders)
|
||||
|
@ -145,8 +145,9 @@ class Controller(object):
|
||||
def parse_qr(self, image):
|
||||
data = b64decode(image['data'])
|
||||
image = PixelImage(data, image['width'], image['height'])
|
||||
x = qrparse.locate_finders(image, 2)
|
||||
return x
|
||||
for qr in qrparse.parse_qr_codes(image, 2):
|
||||
return qrdecode.decode_qr_data(qr)
|
||||
return ""
|
||||
|
||||
|
||||
class PixelImage(object):
|
||||
|
Loading…
Reference in New Issue
Block a user