├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── README.md ├── qreader ├── __init__.py ├── api.py ├── constants.py ├── decoder.py ├── exceptions.py ├── scanner.py ├── spec.py ├── tuples.py ├── utils.py ├── validation.py └── vcard.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── helpers.py ├── resources ├── decoder │ ├── HELLOW-H.txt │ ├── Latin-L.txt │ ├── Numeric-Mod-2-M.txt │ ├── Pi-L.txt │ ├── URL-M.txt │ ├── Version2.txt │ ├── nums-H.txt │ ├── nums-malformed-H.txt │ ├── shintaka-Q.txt │ └── vCard-L.txt └── scanner │ ├── Qr-1-broken-pattern-1.png │ ├── Qr-1-broken-pattern-2.png │ ├── Qr-1-broken-pattern-3.png │ ├── Qr-1-broken-too-light.png │ ├── Qr-1-kanji.png │ ├── Qr-1-noborder.png │ ├── Qr-1.png │ ├── Qr-2-URL.jpg │ ├── Qr-2-alphanumeric.png │ ├── Qr-2-noborder.png │ ├── Qr-2-numeric.png │ ├── Qr-2.png │ ├── Qr-3-Latin-L.jpg │ ├── Qr-3-Numeric-Mod-2-M.gif │ ├── Qr-5-transparent-edges.png │ └── Qr-8-vCard-L.jpg ├── test_api.py ├── test_decoder.py ├── test_scanner.py ├── test_spec.py ├── test_tuples.py ├── test_utils.py ├── test_validation.py └── test_vcard.py /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: R11ozl8Ump8K5LE0fWFaTy8lIoANDl0JV -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | 4 | # Distribution / packaging 5 | .Python 6 | env/ 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | htmlcov/ 29 | .tox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | *,cover 36 | 37 | # Sphinx documentation 38 | docs/_build/ 39 | 40 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "nightly" 9 | # command to install dependencies 10 | install: 11 | - "pip install -r requirements.txt" 12 | - "python setup.py install" 13 | # command to run tests 14 | script: python -m unittest discover 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qreader 2 | A pure python reader for QR codes. Made to be compatible to Python 2.7 to 3.6+. 3 | 4 | We use the easy-to-install Pillow package to load the pictures, so lots of picture formats are supported. 5 | 6 | [![Build Status](https://travis-ci.org/ewino/qreader.svg?branch=master)](https://travis-ci.org/ewino/qreader) 7 | 8 | Status 9 | ----------- 10 | The package is a work in progress. It can currently decode right-side-up QR codes images generated on a computer (not pictures of printed codes). 11 | 12 | Features left to be implemented: 13 | 14 | * QR data error correction (currently being researched and implemented) 15 | * image recognition to recognize QR codes in camera pictures 16 | 17 | Usage 18 | ----------- 19 | qreader.read(source) # source can be a file-like object, a PIL image, or a path to a local file 20 | 21 | For example: 22 | 23 | import qreader 24 | from urllib.request import urlopen 25 | 26 | url = 'https://upload.wikimedia.org/wikipedia/commons/8/8f/Qr-2.png' 27 | data = qreader.read(urlopen(url)) 28 | print(data) # prints "Version 2" 29 | 30 | Any ideas or issues will be gladly received in the issues panel or by PMing me (ewino) 31 | -------------------------------------------------------------------------------- /qreader/__init__.py: -------------------------------------------------------------------------------- 1 | from qreader.api import read 2 | 3 | __author__ = 'ewino' 4 | 5 | -------------------------------------------------------------------------------- /qreader/api.py: -------------------------------------------------------------------------------- 1 | from io import BufferedIOBase 2 | 3 | import six 4 | import PIL.Image 5 | 6 | from qreader.decoder import QRDecoder 7 | from qreader.scanner import ImageScanner 8 | 9 | __author__ = 'ewino' 10 | 11 | __all__ = ['read'] 12 | 13 | 14 | def read(image_or_path): 15 | """ 16 | Accepts either a path to a file, a PIL image, or a file-like object and reads a QR code data from it. 17 | :param str|PIL.Image.Image|file|BufferedIOBase image_or_path: The source containing the QR code. 18 | :return: The data encoded in the QR code. 19 | :rtype: str|unicode|int|qreader.vcard.vCard 20 | """ 21 | if isinstance(image_or_path, (six.string_types + (BufferedIOBase,))) or \ 22 | (six.PY2 and isinstance(image_or_path, file)): 23 | image_or_path = PIL.Image.open(image_or_path) 24 | if isinstance(image_or_path, PIL.Image.Image): 25 | data = ImageScanner(image_or_path) 26 | return QRDecoder(data).get_first() 27 | # result = QRDecoder(data).get_all() 28 | # if len(result) == 0: 29 | # return None 30 | # elif len(result) == 1: 31 | # return result[0] 32 | # else: 33 | # return result 34 | raise TypeError('parameter should be a PIL image object, a file-like object, or a path to an image file') 35 | -------------------------------------------------------------------------------- /qreader/constants.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ewino' 2 | 3 | # QR error correct levels 4 | ERROR_CORRECT_L = 0 5 | ERROR_CORRECT_M = 1 6 | ERROR_CORRECT_Q = 2 7 | ERROR_CORRECT_H = 3 8 | 9 | # QR encoding modes (based on qrcode package) 10 | MODE_NUMBER = 1 11 | MODE_ALPHA_NUM = 2 12 | MODE_BYTES = 4 13 | MODE_KANJI = 8 14 | MODE_ECI = 7 15 | MODE_STRUCTURED_APPEND = 3 16 | 17 | # Encoding mode sizes. 18 | MODE_SIZE_SMALL = { 19 | MODE_NUMBER: 10, 20 | MODE_ALPHA_NUM: 9, 21 | MODE_BYTES: 8, 22 | MODE_KANJI: 8, 23 | } 24 | MODE_SIZE_MEDIUM = { 25 | MODE_NUMBER: 12, 26 | MODE_ALPHA_NUM: 11, 27 | MODE_BYTES: 16, 28 | MODE_KANJI: 10, 29 | } 30 | MODE_SIZE_LARGE = { 31 | MODE_NUMBER: 14, 32 | MODE_ALPHA_NUM: 13, 33 | MODE_BYTES: 16, 34 | MODE_KANJI: 12, 35 | } 36 | 37 | 38 | ALPHANUM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:' 39 | -------------------------------------------------------------------------------- /qreader/decoder.py: -------------------------------------------------------------------------------- 1 | from qreader.constants import MODE_NUMBER, MODE_ALPHA_NUM, ALPHANUM_CHARS, MODE_BYTES, MODE_KANJI, MODE_ECI, \ 2 | MODE_STRUCTURED_APPEND 3 | from qreader.exceptions import IllegalQrMessageModeId 4 | from qreader.spec import bits_for_length 5 | from qreader.utils import ints_to_bytes 6 | from qreader.vcard import vCard 7 | 8 | __author__ = 'ewino' 9 | 10 | 11 | class QRDecoder(object): 12 | 13 | def __init__(self, scanner): 14 | self.scanner = scanner 15 | 16 | @property 17 | def version(self): 18 | return self.scanner.info.version 19 | 20 | def get_first(self): 21 | return self._decode_next_message() 22 | 23 | def __iter__(self): 24 | yield self._decode_next_message() 25 | 26 | def get_all(self): 27 | return list(self) 28 | 29 | def _decode_next_message(self): 30 | mode = self.scanner.read_int(4) 31 | return self._decode_message(mode) 32 | 33 | def _decode_message(self, mode): 34 | if mode == MODE_NUMBER: 35 | message = self._decode_numeric_message() 36 | elif mode == MODE_ALPHA_NUM: 37 | message = self._decode_alpha_num_message() 38 | elif mode == MODE_BYTES: 39 | message = self._decode_bytes_message() 40 | elif mode == MODE_KANJI: 41 | message = self._decode_kanji_message() 42 | elif mode == MODE_STRUCTURED_APPEND: 43 | raise NotImplementedError('Structured append encoding not implemented yet') 44 | elif mode == MODE_ECI: 45 | raise NotImplementedError('Extended Channel Interpretation encoding not implemented yet') 46 | else: 47 | raise IllegalQrMessageModeId(mode) 48 | return message 49 | 50 | def _decode_numeric_message(self): 51 | char_count = self.scanner.read_int(bits_for_length(self.version, MODE_NUMBER)) 52 | val = 0 53 | triples, rest = divmod(char_count, 3) 54 | for _ in range(triples): 55 | val = val * 1000 + self.scanner.read_int(10) 56 | if rest == 2: 57 | val = val * 100 + self.scanner.read_int(7) 58 | elif rest == 1: 59 | val = val * 10 + self.scanner.read_int(4) 60 | 61 | return val 62 | 63 | def _decode_alpha_num_message(self): 64 | char_count = self.scanner.read_int(bits_for_length(self.version, MODE_ALPHA_NUM)) 65 | val = '' 66 | doubles, has_single = divmod(char_count, 2) 67 | for _ in range(doubles): 68 | double = self.scanner.read_int(11) 69 | val += ALPHANUM_CHARS[double // 45] + ALPHANUM_CHARS[double % 45] 70 | if has_single: 71 | val += ALPHANUM_CHARS[self.scanner.read_int(6)] 72 | return val 73 | 74 | def _decode_bytes_message(self): 75 | char_count = self.scanner.read_int(bits_for_length(self.version, MODE_BYTES)) 76 | raw = ints_to_bytes(self.scanner.read_int(8) for _ in range(char_count)) 77 | try: 78 | val = raw.decode('utf-8') 79 | except UnicodeDecodeError: 80 | val = raw.decode('iso-8859-1') 81 | if val.startswith('BEGIN:VCARD\n'): 82 | return vCard.from_text(val) 83 | return val 84 | 85 | def _decode_kanji_message(self): 86 | char_count = self.scanner.read_int(bits_for_length(self.version, MODE_KANJI)) 87 | nums = [] 88 | for _ in range(char_count): 89 | mashed = self.scanner.read_int(13) 90 | num = ((mashed // 0xC0) << 8) + mashed % 0xC0 91 | num += 0x8140 if num < 0x1F00 else 0xC140 92 | nums.extend(divmod(num, 2 ** 8)) 93 | return ints_to_bytes(nums).decode('shift-jis') 94 | -------------------------------------------------------------------------------- /qreader/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class QrReadingException(Exception): 3 | pass 4 | 5 | 6 | class QrImageException(QrReadingException): 7 | pass 8 | 9 | 10 | class QrCorruptError(QrReadingException): 11 | pass 12 | 13 | 14 | class QrImageRecognitionException(QrImageException): 15 | pass 16 | 17 | 18 | class QrFormatError(Exception): 19 | pass 20 | 21 | 22 | class IllegalQrMessageModeId(QrFormatError): 23 | def __init__(self, mode_id): 24 | super(IllegalQrMessageModeId, self).__init__('Unknown mode ID: {0!r:s}'.format(mode_id, )) 25 | 26 | 27 | class IllegalQrVersionError(QrFormatError): 28 | def __init__(self, version): 29 | super(IllegalQrVersionError, self).__init__( 30 | 'Illegal QR version: {0!r:s} (should be integer between 1-40)'.format(version, )) 31 | -------------------------------------------------------------------------------- /qreader/scanner.py: -------------------------------------------------------------------------------- 1 | from collections import Iterator 2 | 3 | from qreader import tuples 4 | from qreader.exceptions import QrImageRecognitionException 5 | from qreader.spec import get_mask_func, FORMAT_INFO_MASK, get_dead_zones, ec_level_from_format_info_code 6 | from qreader.validation import validate_format_info, validate_data 7 | 8 | __author__ = 'ewino' 9 | 10 | WHITE = 0 11 | BLACK = 1 12 | 13 | 14 | class Scanner(object): 15 | def __init__(self): 16 | self._current_index = -1 17 | self._data_len = 0 18 | self._info = None 19 | self.data = None 20 | self._was_read = False 21 | 22 | @property 23 | def info(self): 24 | """ The meta info for the QR code. Reads the code on access if needed. 25 | :rtype: QRCodeInfo 26 | """ 27 | if not self._was_read: 28 | self.read() 29 | return self._info 30 | 31 | def read(self): 32 | self._was_read = True 33 | self.read_info() 34 | self.data = validate_data(self._read_all_data(), self.info.version, self.info.error_correction_level) 35 | self._data_len = len(self.data) 36 | self.reset() 37 | 38 | def read_info(self): 39 | raise NotImplementedError() 40 | 41 | def _read_all_data(self): 42 | raise NotImplementedError() 43 | 44 | # Iteration methods # 45 | 46 | def reset(self): 47 | self._current_index = -1 48 | 49 | def read_bit(self): 50 | if not self._was_read: 51 | self.read() 52 | self._current_index += 1 53 | if self._current_index >= self._data_len: 54 | self._current_index = self._data_len 55 | raise StopIteration() 56 | return self.data[self._current_index] 57 | 58 | def read_int(self, amount_of_bits): 59 | if not self._was_read: 60 | self.read() 61 | val = 0 62 | bits = [self.read_bit() for _ in range(amount_of_bits)] 63 | for bit in bits: 64 | val = (val << 1) + bit 65 | return val 66 | 67 | def __iter__(self): 68 | while True: 69 | try: 70 | yield self.read_bit() 71 | except StopIteration: 72 | return 73 | 74 | 75 | class ImageScanner(Scanner): 76 | def __init__(self, image): 77 | """ 78 | :type image: PIL.Image.Image 79 | :return: 80 | """ 81 | super(ImageScanner, self).__init__() 82 | self.image = image.convert('LA') # gray-scale it baby! 83 | self.mask = None 84 | 85 | def get_mask(self): 86 | mask_func = get_mask_func(self.info.mask_id) 87 | 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)} 88 | 89 | def read_info(self): 90 | info = QRCodeInfo() 91 | info.canvas = self.get_image_borders() 92 | info.block_size = self.get_block_size(info.canvas[:2]) 93 | info.size = int((info.canvas[2] - (info.canvas[0]) + 1) / info.block_size[0]) 94 | info.version = (info.size - 17) // 4 95 | self._info = info 96 | self._read_format_info() 97 | self.mask = self.get_mask() 98 | return info 99 | 100 | def _get_pixel(self, coords): 101 | try: 102 | shade, alpha = self.image.getpixel(coords) 103 | return BLACK if shade < 128 and alpha > 0 else WHITE 104 | except IndexError: 105 | return WHITE 106 | 107 | def get_image_borders(self): 108 | def get_corner_pixel(canvas_corner, vector, max_distance): 109 | for dist in range(max_distance): 110 | for x in range(dist + 1): 111 | coords = (canvas_corner[0] + vector[0] * x, canvas_corner[1] + vector[1] * (dist - x)) 112 | if self._get_pixel(coords) == BLACK: 113 | return coords 114 | raise QrImageRecognitionException( 115 | "Couldn't find one of the edges ({0:s}-{1:s})".format(('top', 'bottom')[vector[1] == -1], 116 | ('left', 'right')[vector[0] == -1])) 117 | 118 | max_dist = min(self.image.width, self.image.height) 119 | min_x, min_y = get_corner_pixel((0, 0), (1, 1), max_dist) 120 | max_x, max_x_y = get_corner_pixel((self.image.width - 1, 0), (-1, 1), max_dist) 121 | max_y_x, max_y = get_corner_pixel((0, self.image.height - 1), (1, -1), max_dist) 122 | if max_x_y != min_y: 123 | raise QrImageRecognitionException('Top-left position pattern not aligned with the top-right one') 124 | if max_y_x != min_x: 125 | raise QrImageRecognitionException('Top-left position pattern not aligned with the bottom-left one') 126 | return min_x, min_y, max_x, max_y 127 | 128 | def get_block_size(self, img_start): 129 | """ 130 | Returns the size in pixels of a single block. 131 | :param tuple[int, int] img_start: The topmost left pixel in the QR (MUST be black or dark). 132 | :return: A tuple of width, height in pixels of a block 133 | :rtype: tuple[int, int] 134 | """ 135 | pattern_size = 7 136 | 137 | left, top = img_start 138 | block_height, block_width = None, None 139 | for i in range(1, (self.image.width - left) // pattern_size): 140 | if self._get_pixel((left + i * pattern_size, top)) == WHITE: 141 | block_width = i 142 | break 143 | for i in range(1, (self.image.height - top) // pattern_size): 144 | if self._get_pixel((left, top + i * pattern_size)) == WHITE: 145 | block_height = i 146 | break 147 | return block_width, block_height 148 | 149 | def _read_format_info(self): 150 | source_1 = (self._get_straight_bits((8, -7), 7, 'd') << 8) + self._get_straight_bits((-1, 8), 8, 'l') 151 | source_2 = (self._get_straight_bits((7, 8), 8, 'l', (1,)) << 8) + self._get_straight_bits((8, 0), 9, 'd', (6,)) 152 | 153 | format_info = validate_format_info(source_1 ^ FORMAT_INFO_MASK, source_2 ^ FORMAT_INFO_MASK) 154 | self.info.error_correction_level = ec_level_from_format_info_code(format_info >> 3) 155 | self.info.mask_id = format_info & 0b111 156 | 157 | def _read_all_data(self): 158 | pos_iterator = QrZigZagIterator(self.info.size, get_dead_zones(self.info.version)) 159 | return [self._get_bit(pos) ^ self.mask[pos] for pos in pos_iterator] 160 | 161 | def _get_bit(self, coords): 162 | x, y = coords 163 | if x < 0: 164 | x += self.info.size 165 | if y < 0: 166 | y += self.info.size 167 | return self._get_pixel(tuples.add(self.info.canvas[:2], tuples.multiply((x, y), self.info.block_size))) 168 | 169 | def _get_straight_bits(self, start, length, direction, skip=()): 170 | """ 171 | Reads several bits from the specified coordinates 172 | :param tuple[int] start: The x, y of the start position 173 | :param int length: the amount of bits to read 174 | :param str direction: d(own) or l(eft) 175 | :param tuple skip: the indexes to skip. they will still be counted on for the length 176 | :return: The bits read as an integer 177 | :rtype: int 178 | """ 179 | result = 0 180 | counted = 0 181 | step = (0, 1) if direction == 'd' else (-1, 0) 182 | for i in range(length): 183 | if i in skip: 184 | start = tuples.add(start, step) 185 | continue 186 | result += self._get_bit(start) << counted 187 | counted += 1 188 | start = tuples.add(start, step) 189 | return result 190 | 191 | 192 | class QrZigZagIterator(Iterator): 193 | def __init__(self, size, dead_zones): 194 | self.size = size 195 | self.ignored_pos = {(x, y) for zone in dead_zones 196 | for x in range(zone[0], zone[2] + 1) 197 | for y in range(zone[1], zone[3] + 1)} 198 | self._current = () 199 | self._scan_direction = 'u' 200 | self._odd_col_modifier = False 201 | self.reset() 202 | 203 | def reset(self): 204 | self._current = (self.size - 2, self.size) 205 | self._scan_direction = 'u' 206 | self._odd_col_modifier = False 207 | 208 | def _advance_pos(self): 209 | pos = self._current 210 | while pos[0] >= 0 and (pos == self._current or pos in self.ignored_pos): 211 | step = (-1, 0) 212 | # We advance a line if we're in an odd column, but if we have the col_modified flag on, we switch it around 213 | advance_line = ((self.size - pos[0]) % 2 == 0) ^ self._odd_col_modifier 214 | if advance_line: 215 | step = (1, -1 if self._scan_direction == 'u' else 1) 216 | # if we're trying to advance a line but we've reached the edge, we should change directions 217 | if (pos[1] == 0 and self._scan_direction == 'u') or \ 218 | (pos[1] == self.size - 1 and self._scan_direction == 'd'): 219 | # swap scan direction 220 | self._scan_direction = 'd' if self._scan_direction == 'u' else 'u' 221 | # go one step left 222 | step = (-1, 0) 223 | # make sure we're not tripping over the timing array 224 | if pos[0] > 0 and all((pos[0] - 1, y) in self.ignored_pos for y in range(self.size)): 225 | step = (-2, 0) 226 | self._odd_col_modifier = not self._odd_col_modifier 227 | pos = tuples.add(pos, step) 228 | self._current = pos 229 | 230 | def __next__(self): 231 | self._advance_pos() 232 | if self._current[0] < 0: 233 | raise StopIteration() 234 | return self._current 235 | 236 | next = __next__ 237 | 238 | 239 | class QRCodeInfo(object): 240 | # number between 1-40 241 | version = 0 242 | 243 | # the error correction level. 244 | error_correction_level = 0 245 | 246 | # the id of the mask (0-7) 247 | mask_id = 0 248 | 249 | # the part of the image that contains the QR code 250 | canvas = (0, 0) 251 | 252 | # the size of each block in pixels 253 | block_size = (0, 0) 254 | 255 | # the amount of blocks at each side of the image (it's always a square) 256 | size = 0 257 | 258 | def __str__(self): 259 | return '' % \ 260 | (self.version, self.error_correction_level, self.mask_id) 261 | -------------------------------------------------------------------------------- /qreader/spec.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | from qreader.constants import MODE_SIZE_SMALL, MODE_SIZE_LARGE, ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, \ 3 | ERROR_CORRECT_H 4 | from qreader.constants import MODE_SIZE_MEDIUM 5 | from qreader.exceptions import QrFormatError, IllegalQrVersionError 6 | from qreader.utils import is_rect_overlapping 7 | 8 | __author__ = 'ewino' 9 | 10 | 11 | FORMAT_INFO_MASK = 0b101010000010010 12 | FORMAT_INFO_BCH_GENERATOR = 0b10100110111 13 | 14 | ALIGNMENT_POSITIONS = [ 15 | [], 16 | [6, 18], 17 | [6, 22], 18 | [6, 26], 19 | [6, 30], 20 | [6, 34], 21 | [6, 22, 38], 22 | [6, 24, 42], 23 | [6, 26, 46], 24 | [6, 28, 50], 25 | [6, 30, 54], 26 | [6, 32, 58], 27 | [6, 34, 62], 28 | [6, 26, 46, 66], 29 | [6, 26, 48, 70], 30 | [6, 26, 50, 74], 31 | [6, 30, 54, 78], 32 | [6, 30, 56, 82], 33 | [6, 30, 58, 86], 34 | [6, 34, 62, 90], 35 | [6, 28, 50, 72, 94], 36 | [6, 26, 50, 74, 98], 37 | [6, 30, 54, 78, 102], 38 | [6, 28, 54, 80, 106], 39 | [6, 32, 58, 84, 110], 40 | [6, 30, 58, 86, 114], 41 | [6, 34, 62, 90, 118], 42 | [6, 26, 50, 74, 98, 122], 43 | [6, 30, 54, 78, 102, 126], 44 | [6, 26, 52, 78, 104, 130], 45 | [6, 30, 56, 82, 108, 134], 46 | [6, 34, 60, 86, 112, 138], 47 | [6, 30, 58, 86, 114, 142], 48 | [6, 34, 62, 90, 118, 146], 49 | [6, 30, 54, 78, 102, 126, 150], 50 | [6, 24, 50, 76, 102, 128, 154], 51 | [6, 28, 54, 80, 106, 132, 158], 52 | [6, 32, 58, 84, 110, 136, 162], 53 | [6, 26, 54, 82, 110, 138, 166], 54 | [6, 30, 58, 86, 114, 142, 170] 55 | ] 56 | 57 | 58 | DATA_BLOCKS_INFO = [ 59 | # For each version: L, M, Q, H: (EC bytes, block size, blocks count, large blocks count) 60 | ((7, 19, 1), (10, 16, 1), (13, 13, 1), (17, 9, 1)), # v1 61 | ((10, 34, 1), (16, 28, 1), (22, 22, 1), (28, 16, 1)), # v2 62 | ((15, 55, 1), (26, 44, 1), (18, 17, 2), (22, 13, 2)), # v3 63 | ((20, 80, 1), (18, 32, 2), (26, 24, 2), (16, 9, 4)), # v4 64 | ((26, 108, 1), (24, 43, 2), (18, 15, 2, 2), (22, 11, 2, 2)), # v5 65 | ((18, 68, 2), (16, 27, 4), (24, 19, 4), (28, 15, 4)), # v6 66 | ((20, 78, 2), (18, 31, 4), (18, 14, 2, 4), (26, 13, 4, 1)), # v7 67 | ((24, 97, 2), (22, 38, 2, 2), (22, 18, 4, 2), (26, 14, 4, 2)), # v8 68 | ((30, 116, 2), (22, 36, 3, 2), (20, 16, 4, 4), (24, 12, 4, 4)), # v9 69 | ((18, 68, 2, 2), (26, 43, 4, 1), (24, 19, 6, 2), (28, 15, 6, 2)), # v10 70 | ((20, 81, 4), (30, 50, 1, 4), (28, 22, 4, 4), (24, 12, 3, 8)), # v11 71 | ((24, 92, 2, 2), (22, 36, 6, 2), (26, 20, 4, 6), (28, 14, 7, 4)), # v12 72 | ((26, 107, 4), (22, 37, 8, 1), (24, 20, 8, 4), (22, 11, 12, 4)), # v13 73 | ((30, 115, 3, 1), (24, 40, 4, 5), (20, 16, 11, 5), (24, 12, 11, 5)), # v14 74 | ((22, 87, 5, 1), (24, 41, 5, 5), (30, 24, 5, 7), (24, 12, 11, 7)), # v15 75 | ((24, 98, 5, 1), (28, 45, 7, 3), (24, 19, 15, 2), (30, 15, 3, 13)), # v16 76 | ((28, 107, 1, 5), (28, 46, 10, 1), (28, 22, 1, 15), (28, 14, 2, 17)), # v17 77 | ((30, 120, 5, 1), (26, 43, 9, 4), (28, 22, 17, 1), (28, 14, 2, 19)), # v18 78 | ((28, 113, 3, 4), (26, 44, 3, 11), (26, 21, 17, 4), (26, 13, 9, 16)), # v19 79 | ((28, 107, 3, 5), (26, 41, 3, 13), (30, 24, 15, 5), (28, 15, 15, 10)), # v20 80 | ((28, 116, 4, 4), (26, 42, 17), (28, 22, 17, 6), (30, 16, 19, 6)), # v21 81 | ((28, 111, 2, 7), (28, 46, 17), (30, 24, 7, 16), (24, 13, 34)), # v22 82 | ((30, 121, 4, 5), (28, 47, 4, 14), (30, 24, 11, 14), (30, 15, 16, 14)), # v23 83 | ((30, 117, 6, 4), (28, 45, 6, 14), (30, 24, 11, 16), (30, 16, 30, 2)), # v24 84 | ((26, 106, 8, 4), (28, 47, 8, 13), (30, 24, 7, 22), (30, 15, 22, 13)), # v25 85 | ((28, 114, 10, 2), (28, 46, 19, 4), (28, 22, 28, 6), (30, 16, 33, 4)), # v26 86 | ((30, 122, 8, 4), (28, 45, 22, 3), (30, 23, 8, 26), (30, 15, 12, 28)), # v27 87 | ((30, 117, 3, 10), (28, 45, 3, 23), (30, 24, 4, 31), (30, 15, 11, 31)), # v28 88 | ((30, 116, 7, 7), (28, 45, 21, 7), (30, 23, 1, 37), (30, 15, 19, 26)), # v29 89 | ((30, 115, 5, 10), (28, 47, 19, 10), (30, 24, 15, 25), (30, 15, 23, 25)), # v30 90 | ((30, 115, 13, 3), (28, 46, 2, 29), (30, 24, 42, 1), (30, 15, 23, 28)), # v31 91 | ((30, 115, 17), (28, 46, 10, 23), (30, 24, 10, 35), (30, 15, 19, 35)), # v32 92 | ((30, 115, 17, 1), (28, 46, 14, 21), (30, 24, 29, 19), (30, 15, 11, 46)), # v33 93 | ((30, 115, 13, 6), (28, 46, 14, 23), (30, 24, 44, 7), (30, 16, 59, 1)), # v34 94 | ((30, 121, 12, 7), (28, 47, 12, 26), (30, 24, 39, 14), (30, 15, 22, 41)), # v35 95 | ((30, 121, 6, 14), (28, 47, 6, 34), (30, 24, 46, 10), (30, 15, 2, 64)), # v36 96 | ((30, 122, 17, 4), (28, 46, 29, 14), (30, 24, 49, 10), (30, 15, 24, 46)), # v37 97 | ((30, 122, 4, 18), (28, 46, 13, 32), (30, 24, 48, 14), (30, 15, 42, 32)), # v38 98 | ((30, 117, 20, 4), (28, 47, 40, 7), (30, 24, 43, 22), (30, 15, 10, 67)), # v39 99 | ((30, 118, 19, 6), (28, 47, 18, 31), (30, 24, 34, 34), (30, 15, 20, 61)), # v40 100 | ] 101 | 102 | 103 | # taken from qrcode package 104 | def get_mask_func(mask_id): 105 | """ 106 | Return the mask function for the given mask pattern. 107 | :param int mask_id: The mask ID in the range 0-7. 108 | """ 109 | id_to_mask = { 110 | 0: lambda i, j: (i + j) % 2 == 0, # 000 111 | 1: lambda i, j: i % 2 == 0, # 001 112 | 2: lambda i, j: j % 3 == 0, # 010 113 | 3: lambda i, j: (i + j) % 3 == 0, # 011 114 | 4: lambda i, j: (i // 2 + j // 3) % 2 == 0, # 100 115 | 5: lambda i, j: (i * j) % 2 + (i * j) % 3 == 0, # 101 116 | 6: lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0, # 110 117 | 7: lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0, # 111 118 | } 119 | if mask_id in id_to_mask: 120 | return id_to_mask[mask_id] 121 | raise QrFormatError("Bad mask pattern: {0!r:s}".format(mask_id)) 122 | 123 | 124 | def mode_sizes_for_version(version): 125 | if version != int(version): 126 | raise IllegalQrVersionError(version) 127 | if 1 <= version <= 9: 128 | return MODE_SIZE_SMALL 129 | elif 10 <= version <= 26: 130 | return MODE_SIZE_MEDIUM 131 | elif 27 <= version <= 40: 132 | return MODE_SIZE_LARGE 133 | raise IllegalQrVersionError(version) 134 | 135 | 136 | def bits_for_length(version, data_mode): 137 | size_mode = mode_sizes_for_version(version) 138 | 139 | if data_mode not in size_mode: 140 | raise QrFormatError(u"Unknown data type ID: {0!r:s}".format(data_mode, )) 141 | 142 | return size_mode[data_mode] 143 | 144 | 145 | def size_by_version(version): 146 | if version < 1 or version > 40 or not version == int(version): 147 | raise IllegalQrVersionError(version) 148 | return 17 + version * 4 149 | 150 | 151 | def ec_level_from_format_info_code(info_ec_code): 152 | levels = { 153 | 0: ERROR_CORRECT_M, 154 | 1: ERROR_CORRECT_L, 155 | 2: ERROR_CORRECT_H, 156 | 3: ERROR_CORRECT_Q 157 | } 158 | return levels[info_ec_code] 159 | 160 | 161 | def get_dead_zones(version): 162 | size = size_by_version(version) 163 | constant_zones = [ 164 | (0, 0, 8, 8), # top left position + format-info 165 | (size - 8, 0, size - 1, 8), # top right position + format-info 166 | (0, size - 8, 7, size - 1), # bottom left position 167 | (8, size - 7, 8, size - 1), # bottom left format info 168 | (8, 6, size - 9, 6), # top timing array 169 | (6, 8, 6, size - 9) # left timing array 170 | ] 171 | 172 | if version >= 7: 173 | constant_zones.append((size - 11, 0, size - 9, 5)) # top version info 174 | constant_zones.append((0, size - 11, 5, size - 9)) # bottom (left) version info 175 | 176 | alignments_zones = [] 177 | alignment_centers = list(permutations(ALIGNMENT_POSITIONS[version - 1], 2)) 178 | alignment_centers.extend((x, x) for x in ALIGNMENT_POSITIONS[version - 1]) 179 | 180 | for center_x, center_y in alignment_centers: 181 | alignment_zone = (center_x - 2, center_y - 2, center_x + 2, center_y + 2) 182 | if all(not is_rect_overlapping(alignment_zone, dead_zone) for dead_zone in constant_zones): 183 | alignments_zones.append(alignment_zone) 184 | return constant_zones + alignments_zones 185 | -------------------------------------------------------------------------------- /qreader/tuples.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ewino' 2 | 3 | 4 | def add(t1, t_or_n): 5 | if isinstance(t_or_n, (int, float)): 6 | return tuple(x + t_or_n for x in t1) 7 | elif isinstance(t_or_n, tuple): 8 | return tuple(x + t_or_n[i] for i, x in enumerate(t1)) 9 | raise TypeError("Can't add a %s to a tuple" % type(t_or_n)) 10 | 11 | 12 | def multiply(t1, t_or_n): 13 | if isinstance(t_or_n, (int, float)): 14 | return tuple(x * t_or_n for x in t1) 15 | elif isinstance(t_or_n, tuple): 16 | return tuple(x * t_or_n[i] for i, x in enumerate(t1)) 17 | raise TypeError("Can't multiply a tuple by a" % type(t_or_n)) 18 | -------------------------------------------------------------------------------- /qreader/utils.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | __author__ = 'ewino' 4 | 5 | 6 | def is_rect_overlapping(rect1, rect2): 7 | h_overlaps = is_range_overlapping((rect1[0], rect1[2]), (rect2[0], rect2[2])) 8 | v_overlaps = is_range_overlapping((rect1[1], rect1[3]), (rect2[1], rect2[3])) 9 | return h_overlaps and v_overlaps 10 | 11 | 12 | def is_range_overlapping(a, b): 13 | """Neither range is completely greater than the other 14 | :param tuple a: first range 15 | :param tuple b: second range 16 | """ 17 | return (a[0] <= b[1]) and (b[0] <= a[1]) 18 | 19 | 20 | def ints_to_bytes(ints): 21 | if six.PY3: 22 | return bytes(ints) 23 | else: 24 | return ''.join(chr(x) for x in ints) 25 | -------------------------------------------------------------------------------- /qreader/validation.py: -------------------------------------------------------------------------------- 1 | from qreader.exceptions import QrCorruptError 2 | from qreader.spec import FORMAT_INFO_BCH_GENERATOR, DATA_BLOCKS_INFO 3 | from reedsolo import RSCodec 4 | 5 | __author__ = 'ewino' 6 | 7 | # Much of the credit goes to the author of the article at 8 | # https://en.wikiversity.org/wiki/Reed-Solomon_codes_for_coders 9 | 10 | 11 | def format_info_check(format_info): 12 | """ Returns 0 if given a complete format info code and it is valid. 13 | Otherwise, returns a positive number. 14 | If given a format info marker padded with 10 bits (e.g. 101010000000000) returns 15 | the corresponding 10-bit error correction code to append to it 16 | :param int format_info: The format info with error correction (15 bits) or without it (5 bits) 17 | :rtype: int 18 | """ 19 | g = FORMAT_INFO_BCH_GENERATOR 20 | for i in range(4, -1, -1): 21 | if format_info & (1 << (i+10)): 22 | format_info ^= g << i 23 | return format_info 24 | 25 | 26 | def hamming_diff(a, b): 27 | """ Calculates the hamming weight of the difference between two number (number of different bits) 28 | :param int a: A number to calculate the diff from 29 | :param int b: A number to calculate the diff from 30 | :return: The amount of different bits 31 | :rtype: int 32 | """ 33 | weight = 0 34 | diff = a ^ b 35 | while diff > 0: 36 | weight += diff & 1 37 | diff >>= 1 38 | return weight 39 | 40 | 41 | def validate_format_info(format_info, second_format_info_sample=None): 42 | """ 43 | Receives one or two copies of a QR format info containing error correction bits, and returns just the format 44 | info bits, after error checking and correction 45 | :param int format_info: The 15-bit format info bits with the error correction info 46 | :param int second_format_info_sample: The secondary 15-bit format info bits with the error correction info 47 | :raise QrCorruptError: in case the format info is too corrupt to singularly verify 48 | :return: The 5-bit (0-31) format info number 49 | :rtype: int 50 | """ 51 | if second_format_info_sample is None: 52 | second_format_info_sample = format_info 53 | if format_info_check(format_info) == format_info_check(second_format_info_sample) == 0: 54 | return format_info >> 10 55 | format_info = (format_info << 15) + second_format_info_sample 56 | 57 | best_format = None 58 | max_distance = 29 59 | for test_format in range(0, 32): 60 | test_code = (test_format << 10) ^ format_info_check(test_format << 10) 61 | test_dist = hamming_diff(format_info, test_code + (test_code << 15)) 62 | 63 | if test_dist < max_distance: 64 | max_distance = test_dist 65 | best_format = test_format 66 | elif test_dist == max_distance: 67 | best_format = None 68 | if best_format is None: 69 | raise QrCorruptError('QR meta-info is too corrupt to read') 70 | return best_format 71 | 72 | 73 | def validate_data(data, version, ec_level): 74 | # block_info = DATA_BLOCKS_INFO[version][ec_level] 75 | # value = RSCodec().decode(data) 76 | return data 77 | -------------------------------------------------------------------------------- /qreader/vcard.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import re 3 | import dateutil.parser 4 | import six 5 | 6 | __author__ = 'ewino' 7 | 8 | 9 | # A very small vCard parsing implementation, because vobject's looks quite very bloated. 10 | # Will probably be replaced by vobject or a lighter library (if I find one) 11 | 12 | 13 | class vCard(object): 14 | 15 | def __init__(self): 16 | self.addresses = [] 17 | self.agent = None 18 | self.anniversary = None 19 | self.bday = None 20 | self.categories = [] 21 | self.emails = [] 22 | self.formatted_name = None 23 | self.gender = None 24 | self.geo = None 25 | self.impp = [] 26 | self.key = None 27 | self.address_labels = [] 28 | self.lang = None 29 | self.logo = None 30 | self.name = None 31 | self.note = None 32 | self.nickname = None 33 | self.org = None 34 | self.photo = None 35 | self.rev = None 36 | self.role = None 37 | self.phones = [] 38 | self.tz = None 39 | 40 | @classmethod 41 | def from_text(cls, txt): 42 | lines = txt.splitlines() 43 | """ :type: list[str] """ 44 | if lines[0] == 'BEGIN:VCARD' and lines[-1] == 'END:VCARD': 45 | lines = lines[1:-1] 46 | else: 47 | raise ValueError('Not a valid vCard format') 48 | 49 | key_field_map = { 50 | 'ADR': 'addresses', 51 | 'AGENT': 'agent', 52 | 'ANNIVERSARY': ('anniversary', datetime), 53 | 'BDAY': ('bday', datetime), 54 | 'CATEGORIES': ('categories', list), 55 | 'EMAIL': 'emails', 56 | 'FN': 'formatted_name', 57 | 'GENDER': 'gender', 58 | 'GEO': 'geo', 59 | 'IMPP': 'impp', 60 | 'KEY': 'key', 61 | 'LABEL': 'address_labels', 62 | 'LANG': 'lang', 63 | 'LOGO': 'logo', 64 | 'N': 'name', 65 | 'NICKNAME': 'nickname', 66 | 'NOTE': 'note', 67 | 'ORG': 'org', 68 | 'PHOTO': 'photo', 69 | 'REV': ('rev', datetime), 70 | 'TEL': 'phones', 71 | 'ROLE': 'role', 72 | 'TZ': 'tz', 73 | } 74 | ignore_fields = {'VERSION'} 75 | 76 | card = cls() 77 | 78 | for line in lines: 79 | if not line.strip(): 80 | continue 81 | key, val = line.split(':', 1) 82 | if key in ignore_fields: 83 | continue 84 | if ';' in key: 85 | key, extra = re.findall('^(\w+);(?:\w+=)?(.+)$', key)[0] 86 | val = '%s;%s' % (extra, val) 87 | field = key_field_map.get(key) 88 | if not field: 89 | raise TypeError('Unknown vCard field: %s. This implementation only supports basic properties' % key) 90 | if isinstance(field, tuple): 91 | field, tpe = field 92 | if tpe == datetime: 93 | val = dateutil.parser.parse(val) 94 | elif tpe == list: 95 | val = val.split(',') 96 | if isinstance(val, six.string_types) and ';' in val: 97 | val = tuple(val.split(';')) 98 | 99 | if isinstance(getattr(card, field), list): 100 | prop = getattr(card, field) 101 | if isinstance(val, list): 102 | prop.extend(val) 103 | else: 104 | prop.append(val) 105 | else: 106 | setattr(card, field, val) 107 | return card 108 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | six 3 | python-dateutil 4 | reedsolo 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import glob, os 2 | from distutils.core import setup 3 | 4 | PACKAGE_NAME = "qreader" 5 | 6 | setup(name=PACKAGE_NAME, 7 | version="0.1", 8 | description="A pure python reader for QR codes.", 9 | url="https://github.com/ewino/qreader/", 10 | author="Ehud Winograd", 11 | maintainer="Ehud Winograd", 12 | long_description='A pure python reader for QR codes. Made to be compatible to Python 2.7 to 3.5+.', 13 | platforms=["Unix", "Windows"], 14 | packages=['qreader'], 15 | data_files=[('', ['README.md'] + glob.glob('doc/*')), 16 | (os.path.join('tests'), glob.glob('tests/*.py')), 17 | (os.path.join('tests', 'resources', 'decoder'), glob.glob('tests/resources/decoder/*')), 18 | (os.path.join('tests', 'resources', 'scanner'), glob.glob('tests/resources/scanner/*'))], 19 | requires=['Pillow', 'six', 'python_dateutil', 'reedsolo']) 20 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ewino' 2 | 3 | # This is needed so inner-imports will work (e.g. import tests.helpers) 4 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | import os 4 | from unittest import TestCase as BaseTestCase 5 | 6 | from PIL import Image 7 | from qreader.constants import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H 8 | 9 | 10 | class Example(object): 11 | def __init__(self, img_name, version, ec_mode, mask, txt_name=None): 12 | self.img_name = img_name 13 | if txt_name and '.' not in txt_name: 14 | txt_name += '.txt' 15 | self.txt_name = txt_name 16 | self.version = version 17 | self.ec_mode = ec_mode 18 | self.mask = mask 19 | self._validate() 20 | 21 | def _validate(self): 22 | if self.img_name: 23 | assert os.path.exists(self.img_res_path), 'Example image file missing ({0})'.format(self.img_res_path) 24 | if self.txt_name: 25 | assert os.path.exists(self.txt_res_path), 'Example text file missing ({0})'.format(self.txt_res_path) 26 | 27 | @property 28 | def img_res_path(self): 29 | if not self.img_name: 30 | raise ValueError('Resource does not have image file') 31 | return os.path.join(os.path.dirname(__file__), 'resources', 'scanner', self.img_name) 32 | 33 | def get_img_res(self): 34 | return Image.open(self.img_res_path) 35 | 36 | @property 37 | def txt_res_path(self): 38 | if not self.txt_name: 39 | raise ValueError('Resource does not have content txt file') 40 | return os.path.join(os.path.dirname(__file__), 'resources', 'decoder', self.txt_name) 41 | 42 | 43 | class EXAMPLES: 44 | simple_1 = Example('Qr-1.png', 1, ERROR_CORRECT_H, mask=1) # Ver1 45 | simple_2 = Example('Qr-2.png', 2, ERROR_CORRECT_H, mask=2, txt_name='Version2.txt') # Version 2 46 | # TODO: LATIN-L doesn't match it's text file (text file version preferred). Should regenerate it (ewino@2016-01-30) 47 | simple_3 = Example('Qr-3-Latin-L.jpg', 3, ERROR_CORRECT_L, mask=4, txt_name='Latin-L.txt') # Hello! 48 | 49 | noborder_1 = Example('Qr-1-noborder.png', 1, ERROR_CORRECT_H, mask=1) # Ver1 50 | noborder_2 = Example('Qr-2-noborder.png', 2, ERROR_CORRECT_L, mask=2, txt_name='Pi-L.txt') # pi=3.14159265358979 51 | transparent_border = Example('Qr-5-transparent-edges.png', 5, ERROR_CORRECT_H, mask=1) # ??? 52 | 53 | kanji = Example('Qr-1-kanji.png', 1, ERROR_CORRECT_Q, mask=2, txt_name='shintaka-Q.txt') # 新高 54 | alphanum = Example('Qr-2-alphanumeric.png', 2, ERROR_CORRECT_H, mask=5, txt_name='HELLOW-H.txt') # HELLO WORLD 55 | numeric = Example('Qr-2-numeric.png', 2, ERROR_CORRECT_H, mask=4, txt_name='nums-H.txt') # 1112223330020159990 56 | numeric_2 = Example('Qr-3-Numeric-Mod-2-M.gif', 3, ERROR_CORRECT_M, mask=4, txt_name='Numeric-Mod-2-M.txt') # 55 57 | url = Example('Qr-2-URL.jpg', 2, ERROR_CORRECT_L, mask=2, txt_name='URL-M.txt') # http://google.co.tz 58 | # TODO: vCard-L doesn't match it's text file (text file version preferred). Should regenerate it (ewino@2016-01-30) 59 | vcard = Example('Qr-8-vCard-L.jpg', 8, ERROR_CORRECT_L, mask=3, txt_name='vCard-L.txt') # vCard 60 | 61 | broken_pattern_1 = Example('Qr-1-broken-pattern-1.png', 1, ERROR_CORRECT_Q, mask=1) # top-left noise 62 | broken_pattern_2 = Example('Qr-1-broken-pattern-2.png', 1, ERROR_CORRECT_Q, mask=1) # top-right noise 63 | broken_pattern_3 = Example('Qr-1-broken-pattern-3.png', 1, ERROR_CORRECT_Q, mask=1) # bottom-right noise 64 | broken_too_light = Example('Qr-1-broken-too-light.png', 1, ERROR_CORRECT_Q, mask=1) # code is in very light gray 65 | 66 | broken_message_mode = Example(None, 2, ERROR_CORRECT_Q, mask=4, txt_name='nums-malformed-H.txt') # 1112223330... 67 | 68 | 69 | class TestCase(BaseTestCase): 70 | 71 | def assertRaisesMsg(self, exc_type, func, exc_msg, *args, **kwargs): 72 | with self.assertRaises(exc_type) as cm: 73 | func(*args, **kwargs) 74 | self.assertEqual(cm.exception.args[0], exc_msg) 75 | -------------------------------------------------------------------------------- /tests/resources/decoder/HELLOW-H.txt: -------------------------------------------------------------------------------- 1 | 001000000101101100001011011110001101000101110010110111000100110101000011010000001110110000010001111011000001000111101100000100011010000001001000111110010010001100001010000001101100001100011111010111100001101101110001001001010111110010010001010000100101101000110110101010000011100010100010000000110010011011010001000101001101000111111001101110111001001011111111 -------------------------------------------------------------------------------- /tests/resources/decoder/Latin-L.txt: -------------------------------------------------------------------------------- 1 | 0100000001111111101100100000111111000010000011111101001000001111111000001110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000100000000010011101101111101011101101111110100111001111001001000000011110010001110000101101111101100010011110001001000000110000000 -------------------------------------------------------------------------------- /tests/resources/decoder/Numeric-Mod-2-M.txt: -------------------------------------------------------------------------------- 1 | 0001000000001001101110000000000011101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001010110100101111110001111101111111101110001001110100001010101001011000100010010010100000101100000001000001001011001000110100001001100111100111001000010011010010111000111001110101100000111001000100011001110000011111111 -------------------------------------------------------------------------------- /tests/resources/decoder/Pi-L.txt: -------------------------------------------------------------------------------- 1 | 010000010011011100000110100100111101001100110010111000110001001101000011000100110101001110010011001000110110001101010011001100110101001110000011100100110111001110010000111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011000001000100001101110011000110000111101110001100010010110010001110010011001011111110000000 -------------------------------------------------------------------------------- /tests/resources/decoder/URL-M.txt: -------------------------------------------------------------------------------- 1 | 010000010011011010000111010001110100011100000011101000101111001011110110011101101111011011110110011101101100011001010010111001100011011011110010111001110100011110100000111011000001000111101100000100011110110000010001111011000001000111101100000100011110110000010001111011001110111001001100000101011101011001001011100101001111000111010001110010110011111000000000 -------------------------------------------------------------------------------- /tests/resources/decoder/Version2.txt: -------------------------------------------------------------------------------- 1 | 010000001001010101100110010101110010011100110110100101101111011011100010000000110010000011101100000100011110110000010001111011000000110001011010111001000011110100011110010011001001000001100111000111001100010101001010001011001101110100000010001010101011011111111110110101101100100000101010010011011000100010100110100001001001101110011110000011100101010100000000 -------------------------------------------------------------------------------- /tests/resources/decoder/nums-H.txt: -------------------------------------------------------------------------------- 1 | 000100000100110001101111001101111001010011010000000010000000111111111001110000000000000011101100000100011110110000010001111011000010100111110111101101100101110111111001110010111101011000010110111111000110011111111010101010100100010000001010011101100111110100010000110000000000001000000011011101100100111000111101011000100110101010111111011010111001000101111111 -------------------------------------------------------------------------------- /tests/resources/decoder/nums-malformed-H.txt: -------------------------------------------------------------------------------- 1 | 111100000100110001101111001101111001010011010000000010000000111111111001110000000000000011101100000100011110110000010001111011000010100111110111101101100101110111111001110010111101011000010110111111000110011111111010101010100100010000001010011101100111110100010000110000000000001000000011011101100100111000111101011000100110101010111111011010111001000101111111 -------------------------------------------------------------------------------- /tests/resources/decoder/shintaka-Q.txt: -------------------------------------------------------------------------------- 1 | 10000000001001011010101100100101000010000000000011101100000100011110110000010001111011000001000111101100011010010111110111100001110011000101101100101011010000111110010000100001110010110010100111010111110010111 -------------------------------------------------------------------------------- /tests/resources/decoder/vCard-L.txt: -------------------------------------------------------------------------------- 1 | 0100010010100100001001000101010001110100100101001110001110100101011001000011010000010101001001000100000010100101011001000101010100100101001101001001010011110100111000111010001100110010111000110000000010100100111000111010010000100110110001100001011000100110110001100001001110110100001001101100011000010010000001000010011011000110000100001010010101000100010101001100001110110101010001011001010100000100010100111101010000110100010101001100010011000011101000110001001100100011001100110100001101010011011000110111001110000011100100001010010001010100111001000100001110100101011001000011010000010101001001000100000011101100000100011110110000010001011001011100110001111100111111000110001101011011111001011110001110101110011101111100101011011111010010010111101110000001000111101000000100001001101000011110000000000000 -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-broken-pattern-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-broken-pattern-1.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-broken-pattern-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-broken-pattern-2.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-broken-pattern-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-broken-pattern-3.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-broken-too-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-broken-too-light.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-kanji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-kanji.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1-noborder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1-noborder.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-1.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-2-URL.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-2-URL.jpg -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-2-alphanumeric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-2-alphanumeric.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-2-noborder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-2-noborder.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-2-numeric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-2-numeric.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-2.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-3-Latin-L.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-3-Latin-L.jpg -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-3-Numeric-Mod-2-M.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-3-Numeric-Mod-2-M.gif -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-5-transparent-edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-5-transparent-edges.png -------------------------------------------------------------------------------- /tests/resources/scanner/Qr-8-vCard-L.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewino/qreader/a586a8a099375ff2bf7a6491b0957c05a657701e/tests/resources/scanner/Qr-8-vCard-L.jpg -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import six 2 | from PIL import Image 3 | 4 | from qreader.api import read 5 | from tests.helpers import TestCase, EXAMPLES 6 | 7 | 8 | class TestRead(TestCase): 9 | def test_with_image(self): 10 | read(Image.open(EXAMPLES.simple_1.img_res_path)) 11 | 12 | def test_with_path(self): 13 | read(EXAMPLES.simple_1.img_res_path) 14 | 15 | def test_with_file(self): 16 | with open(EXAMPLES.simple_1.img_res_path, 'rb') as f: 17 | read(f) 18 | 19 | def test_with_weird_values(self): 20 | for val in (None, ['bread', 'eggs', 'sugar'], TestRead, six.moves.xrange(12)): 21 | self.assertRaises(TypeError, read, val) 22 | 23 | def test_with_wrong_path(self): 24 | exception_type = FileNotFoundError if six.PY3 else IOError 25 | self.assertRaises(exception_type, read, EXAMPLES.simple_1.img_res_path.replace('.', '-')) 26 | -------------------------------------------------------------------------------- /tests/test_decoder.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | from qreader.constants import MODE_ECI, MODE_STRUCTURED_APPEND, ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, \ 4 | ERROR_CORRECT_H 5 | from qreader.decoder import QRDecoder 6 | from qreader.exceptions import IllegalQrMessageModeId 7 | from qreader.scanner import QRCodeInfo, Scanner 8 | from qreader.vcard import vCard 9 | from tests.helpers import TestCase, EXAMPLES 10 | 11 | __author__ = 'ewino' 12 | 13 | 14 | class TextFileScanner(Scanner): 15 | def __init__(self, file_path, version, ec_level): 16 | self._data_path = file_path 17 | self._version = version 18 | self._ec_level = ec_level 19 | super(TextFileScanner, self).__init__() 20 | 21 | def read_info(self): 22 | self._info = QRCodeInfo() 23 | self.info.version = self._version 24 | self.info.error_correction_level = self._ec_level 25 | 26 | def _read_all_data(self): 27 | with open(self._data_path, 'r') as f: 28 | return [int(x) for x in f.read()] 29 | 30 | 31 | class TestDecoder(TestCase): 32 | def _get_decoder(self, res): 33 | """ 34 | :type res: tests.helpers.Example 35 | :rtype: QRDecoder 36 | """ 37 | return QRDecoder(TextFileScanner(res.txt_res_path, res.version, res.ec_mode)) 38 | 39 | def test_numeric(self): 40 | self.assertEqual(1112223330020159990, self._get_decoder(EXAMPLES.numeric).get_first()) 41 | self.assertEqual(55, self._get_decoder(EXAMPLES.numeric_2).get_first()) 42 | 43 | def test_alphanumeric(self): 44 | self.assertEqual('HELLO WORLD', self._get_decoder(EXAMPLES.alphanum).get_first()) 45 | self.assertEqual('http://google.co.tz', self._get_decoder(EXAMPLES.url).get_first()) 46 | 47 | def test_bytes(self): 48 | self.assertEqual('pi=3.14159265358979', self._get_decoder(EXAMPLES.noborder_2).get_first()) 49 | self.assertEqual('Version 2', self._get_decoder(EXAMPLES.simple_2).get_first()) 50 | self.assertEqual(u'û ü ý þ', self._get_decoder(EXAMPLES.simple_3).get_first()) 51 | 52 | def test_kanji(self): 53 | self.assertEqual(u'新高', self._get_decoder(EXAMPLES.kanji).get_first()) 54 | 55 | def test_unknown_type(self): 56 | self.assertRaisesMsg(IllegalQrMessageModeId, self._get_decoder(EXAMPLES.broken_message_mode).get_first, 57 | 'Unknown mode ID: 15') 58 | 59 | def test_eci(self): 60 | self.assertRaises(NotImplementedError, lambda: self._get_decoder(EXAMPLES.kanji)._decode_message(MODE_ECI)) 61 | 62 | def test_multi(self): 63 | self.assertRaises(NotImplementedError, 64 | lambda: self._get_decoder(EXAMPLES.kanji)._decode_message(MODE_STRUCTURED_APPEND)) 65 | 66 | def test_vcard(self): 67 | card = self._get_decoder(EXAMPLES.vcard).get_first() 68 | self.assertTrue(vCard, type(card)) 69 | self.assertEqual(('Blabla', 'Bla Bla'), card.name) 70 | self.assertEqual(('CELL', '123456789'), card.phones[0]) 71 | 72 | def test_iteration(self): 73 | messages = self._get_decoder(EXAMPLES.alphanum).get_all() 74 | messages2 = [] 75 | for message in self._get_decoder(EXAMPLES.alphanum): 76 | messages2.append(message) 77 | self.assertEqual(messages, messages2) 78 | self.assertEqual(1, len(messages)) 79 | self.assertEqual('HELLO WORLD', messages[0]) 80 | -------------------------------------------------------------------------------- /tests/test_scanner.py: -------------------------------------------------------------------------------- 1 | from qreader.exceptions import QrImageRecognitionException 2 | from qreader.scanner import ImageScanner, Scanner 3 | from tests.helpers import TestCase, EXAMPLES 4 | 5 | __author__ = 'ewino' 6 | 7 | 8 | class TestScanner(TestCase): 9 | 10 | def _get_res_scanner(self, res): 11 | """ 12 | :type res: tests.helpers.Example 13 | :rtype: qreader.scanner.ImageScanner 14 | """ 15 | return ImageScanner(res.get_img_res()) 16 | 17 | def _assert_info(self, res): 18 | info = self._get_res_scanner(res).info 19 | self.assertEqual(res.version, info.version) 20 | self.assertEqual(res.ec_mode, info.error_correction_level) 21 | self.assertEqual(res.mask, info.mask_id) 22 | 23 | def test_canvas_size(self): 24 | self.assertEqual((0, 0, 146, 146), self._get_res_scanner(EXAMPLES.noborder_1).info.canvas) 25 | self.assertEqual((0, 0, 199, 199), self._get_res_scanner(EXAMPLES.noborder_2).info.canvas) 26 | self.assertEqual((36, 36, 182, 182), self._get_res_scanner(EXAMPLES.simple_1).info.canvas) 27 | self.assertEqual((35, 35, 184, 184), self._get_res_scanner(EXAMPLES.simple_2).info.canvas) 28 | self.assertEqual((14, 14, 235, 235), self._get_res_scanner(EXAMPLES.transparent_border).info.canvas) 29 | 30 | def test_broken_canvas_sizes(self): 31 | self.assertRaisesMsg(QrImageRecognitionException, self._get_res_scanner(EXAMPLES.broken_pattern_1).read, 32 | 'Top-left position pattern not aligned with the top-right one') 33 | self.assertRaisesMsg(QrImageRecognitionException, self._get_res_scanner(EXAMPLES.broken_pattern_2).read, 34 | 'Top-left position pattern not aligned with the top-right one') 35 | self.assertRaisesMsg(QrImageRecognitionException, self._get_res_scanner(EXAMPLES.broken_pattern_3).read, 36 | 'Top-left position pattern not aligned with the bottom-left one') 37 | self.assertRaisesMsg(QrImageRecognitionException, self._get_res_scanner(EXAMPLES.broken_too_light).read, 38 | "Couldn't find one of the edges (top-left)") 39 | 40 | def test_info(self): 41 | self._assert_info(EXAMPLES.simple_1) 42 | self._assert_info(EXAMPLES.simple_2) 43 | self._assert_info(EXAMPLES.noborder_1) 44 | self._assert_info(EXAMPLES.noborder_2) 45 | self._assert_info(EXAMPLES.kanji) 46 | self._assert_info(EXAMPLES.numeric) 47 | self._assert_info(EXAMPLES.alphanum) 48 | self._assert_info(EXAMPLES.vcard) 49 | self._assert_info(EXAMPLES.transparent_border) 50 | 51 | def test_jpg(self): 52 | self._assert_info(EXAMPLES.simple_3) 53 | self._assert_info(EXAMPLES.url) 54 | 55 | def test_gif(self): 56 | self._assert_info(EXAMPLES.numeric_2) 57 | 58 | def test_size(self): 59 | self.assertEqual(209, len(list(self._get_res_scanner(EXAMPLES.simple_1)))) 60 | self.assertEqual(209, len(list(self._get_res_scanner(EXAMPLES.noborder_1)))) 61 | self.assertEqual(360, len(list(self._get_res_scanner(EXAMPLES.simple_2)))) 62 | self.assertEqual(360, len(list(self._get_res_scanner(EXAMPLES.noborder_2)))) 63 | # TODO: Add check for version >= 7 (ewino@2015-08-30) 64 | 65 | def test_info_str(self): 66 | for res in (EXAMPLES.noborder_1, EXAMPLES.simple_2): 67 | self.assertEqual('' % (res.version, res.ec_mode, res.mask), 68 | str(self._get_res_scanner(res).info)) 69 | 70 | 71 | class TestAbstractScanner(TestCase): 72 | """ Made mostly to appease the coverage runner :P """ 73 | def test_that_its_abstract(self): 74 | scanner = Scanner() 75 | self.assertRaises(NotImplementedError, scanner._read_all_data) 76 | self.assertRaises(NotImplementedError, scanner.read_info) 77 | -------------------------------------------------------------------------------- /tests/test_spec.py: -------------------------------------------------------------------------------- 1 | from qreader.constants import MODE_SIZE_SMALL, MODE_SIZE_MEDIUM, MODE_SIZE_LARGE, MODE_KANJI, MODE_ALPHA_NUM, \ 2 | MODE_NUMBER, MODE_BYTES 3 | from qreader.exceptions import IllegalQrVersionError, QrFormatError 4 | from qreader.spec import get_mask_func, mode_sizes_for_version, bits_for_length, get_dead_zones, DATA_BLOCKS_INFO 5 | from tests.helpers import TestCase 6 | 7 | __author__ = 'ewino' 8 | 9 | 10 | class TestMasks(TestCase): 11 | def test_mask_bounds(self): 12 | for mask_id in range(8): 13 | mask_func = get_mask_func(mask_id) 14 | for i in range(177): 15 | for j in range(177): 16 | self.assertIn(mask_func(i, j), (0, 1)) 17 | 18 | def test_nonexistent_masks(self): 19 | self.assertRaisesMsg(QrFormatError, lambda: get_mask_func(-1), 'Bad mask pattern: -1') 20 | self.assertRaisesMsg(QrFormatError, lambda: get_mask_func(8), 'Bad mask pattern: 8') 21 | 22 | def test_mask_formula_correctness(self): 23 | mask_samples = { 24 | 0: '1010101010101010101010101010101010101010101010101', 25 | 1: '1111111000000011111110000000111111100000001111111', 26 | 2: '1001001100100110010011001001100100110010011001001', 27 | 3: '1001001001001001001001001001001001001001001001001', 28 | 4: '1110001111000100011100001110111000111100010001110', 29 | 5: '1111111100000110010011010101100100110000011111111', 30 | 6: '1111111111000111011011010101101101110001111111111', 31 | 7: '1010101000111010001110101010111000101110001010101', 32 | } 33 | 34 | for mask_id in range(8): 35 | mask_func = get_mask_func(mask_id) 36 | mask_result = '' 37 | for i in range(7): 38 | for j in range(7): 39 | mask_result += '1' if mask_func(i, j) else '0' 40 | self.assertEqual(mask_samples[mask_id], mask_result) 41 | 42 | 43 | class TestCharCounts(TestCase): 44 | 45 | def test_size_for_version(self): 46 | self.assertEqual(MODE_SIZE_SMALL, mode_sizes_for_version(1)) 47 | self.assertEqual(MODE_SIZE_SMALL, mode_sizes_for_version(2)) 48 | self.assertEqual(MODE_SIZE_SMALL, mode_sizes_for_version(5)) 49 | self.assertEqual(MODE_SIZE_SMALL, mode_sizes_for_version(9)) 50 | self.assertEqual(MODE_SIZE_MEDIUM, mode_sizes_for_version(10)) 51 | self.assertEqual(MODE_SIZE_MEDIUM, mode_sizes_for_version(11)) 52 | self.assertEqual(MODE_SIZE_MEDIUM, mode_sizes_for_version(12)) 53 | self.assertEqual(MODE_SIZE_MEDIUM, mode_sizes_for_version(20)) 54 | self.assertEqual(MODE_SIZE_MEDIUM, mode_sizes_for_version(26)) 55 | self.assertEqual(MODE_SIZE_LARGE, mode_sizes_for_version(27)) 56 | self.assertEqual(MODE_SIZE_LARGE, mode_sizes_for_version(28)) 57 | self.assertEqual(MODE_SIZE_LARGE, mode_sizes_for_version(39)) 58 | self.assertEqual(MODE_SIZE_LARGE, mode_sizes_for_version(40)) 59 | 60 | def test_size_for_illegal_versions(self): 61 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(0)) 62 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(-1)) 63 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(41)) 64 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(0.5)) 65 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(-1.5)) 66 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(10.5)) 67 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(9.5)) 68 | self.assertRaises(IllegalQrVersionError, lambda: mode_sizes_for_version(40.5)) 69 | 70 | def test_char_counts(self): 71 | self.assertEqual(8, bits_for_length(8, MODE_KANJI)) 72 | self.assertEqual(9, bits_for_length(8, MODE_ALPHA_NUM)) 73 | self.assertEqual(10, bits_for_length(8, MODE_NUMBER)) 74 | 75 | self.assertEqual(10, bits_for_length(18, MODE_KANJI)) 76 | self.assertEqual(11, bits_for_length(18, MODE_ALPHA_NUM)) 77 | self.assertEqual(12, bits_for_length(18, MODE_NUMBER)) 78 | 79 | self.assertEqual(12, bits_for_length(28, MODE_KANJI)) 80 | self.assertEqual(13, bits_for_length(28, MODE_ALPHA_NUM)) 81 | self.assertEqual(14, bits_for_length(28, MODE_NUMBER)) 82 | 83 | self.assertEqual(8, bits_for_length(8, MODE_BYTES)) 84 | self.assertEqual(16, bits_for_length(18, MODE_BYTES)) 85 | self.assertEqual(16, bits_for_length(28, MODE_BYTES)) 86 | 87 | def test_illegal_char_counts(self): 88 | # illegal version 89 | self.assertRaises(IllegalQrVersionError, lambda: bits_for_length(0, 1)) 90 | self.assertRaises(IllegalQrVersionError, lambda: bits_for_length(41, 1)) 91 | self.assertRaises(IllegalQrVersionError, lambda: bits_for_length(8.5, 1)) 92 | 93 | # illegal modes 94 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 0), 'Unknown data type ID: 0') 95 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, -1), 'Unknown data type ID: -1') 96 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 3), 'Unknown data type ID: 3') 97 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 7), 'Unknown data type ID: 7') 98 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 9), 'Unknown data type ID: 9') 99 | 100 | # downright weird modes 101 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 3.5), 'Unknown data type ID: 3.5') 102 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, None), 'Unknown data type ID: None') 103 | self.assertRaisesMsg(QrFormatError, lambda: bits_for_length(8, 'hi'), "Unknown data type ID: 'hi'") 104 | 105 | 106 | class TestDeadZones(TestCase): 107 | REGULAR_ZONES_COUNT = 6 108 | 109 | def test_normal_dead_zones(self): 110 | self.assertEqual(self.REGULAR_ZONES_COUNT, len(get_dead_zones(1))) 111 | 112 | def test_alignment_patterns_amount(self): 113 | amounts = sum(([p**2] * x for p, x in enumerate([1, 5, 7, 7, 7, 7, 6])), []) # 1*0, 5*1, 7*9, 7*16, 7*25... 114 | self.assertEqual(40, len(amounts)) 115 | for version, amount in enumerate(amounts, start=1): 116 | regular_zones_count = self.REGULAR_ZONES_COUNT + (2 if version >= 7 else 0) 117 | self.assertEqual(amount, len(get_dead_zones(version)) - regular_zones_count) 118 | 119 | def test_illegal_versions(self): 120 | self.assertRaises(IllegalQrVersionError, lambda: get_dead_zones(-1)) 121 | self.assertRaises(IllegalQrVersionError, lambda: get_dead_zones(0)) 122 | self.assertRaises(IllegalQrVersionError, lambda: get_dead_zones(1.5)) 123 | self.assertRaises(IllegalQrVersionError, lambda: get_dead_zones(41)) 124 | 125 | 126 | class TestBlockInfo(TestCase): 127 | 128 | def test_data_structure(self): 129 | self.assertEqual(40, len(DATA_BLOCKS_INFO)) 130 | for version, ec_levels_info in enumerate(DATA_BLOCKS_INFO, start=1): 131 | self.assertEqual(4, len(ec_levels_info)) 132 | for ec_level, data in enumerate(ec_levels_info): 133 | self.assertIsInstance(data, tuple) 134 | self.assertIn(len(data), (3, 4)) 135 | for datum in data: 136 | self.assertIsInstance(datum, int) 137 | 138 | def test_data_amount_consistent_per_version(self): 139 | for version, ec_levels_info in enumerate(DATA_BLOCKS_INFO, start=1): 140 | data_amounts = set() 141 | for level_info in ec_levels_info: 142 | amount = (level_info[0] + level_info[1]) * level_info[2] 143 | if len(level_info) > 3: 144 | amount += (level_info[0] + level_info[1] + 1) * level_info[3] 145 | data_amounts.add(amount) 146 | self.assertEqual(1, len(data_amounts)) 147 | -------------------------------------------------------------------------------- /tests/test_tuples.py: -------------------------------------------------------------------------------- 1 | from qreader import tuples 2 | from tests.helpers import TestCase 3 | 4 | __author__ = 'ewino' 5 | 6 | 7 | class TestAdd(TestCase): 8 | 9 | def test_scalar(self): 10 | t = (-1, 0, 1, 2.5, 3) 11 | self.assertEqual((-2, -1, 0, 1.5, 2), tuples.add(t, -1)) 12 | self.assertEqual((0, 1, 2, 3.5, 4), tuples.add(t, 1)) 13 | self.assertEqual((.5, 1.5, 2.5, 4, 4.5), tuples.add(t, 1.5)) 14 | self.assertEqual(t, tuples.add(t, 0)) 15 | 16 | def test_tuple(self): 17 | t = (-1, 0, 1, 2.5, 3) 18 | self.assertEqual((-2, 0, 2, 5, 6), tuples.add(t, (-1, 0, 1, 2.5, 3))) 19 | self.assertEqual((0,) * 5, tuples.add(t, (1, 0, -1, -2.5, -3))) 20 | self.assertEqual(t, tuples.add(t, (0,) * 5)) 21 | 22 | def test_weird_addition(self): 23 | t = (-1, 0, 1, 2.5, 3) 24 | self.assertRaises(TypeError, lambda: tuples.add(t, 'hello')) 25 | self.assertRaises(TypeError, lambda: tuples.add(t, None)) 26 | 27 | def test_too_short_tuple(self): 28 | t = (-1, 0, 1, 2.5, 3) 29 | self.assertRaises(IndexError, lambda: tuples.add(t, ())) 30 | 31 | 32 | class TestMultiply(TestCase): 33 | 34 | def test_scalar(self): 35 | t = (-1, 0, 1, 2.5, 3) 36 | self.assertEqual((1, 0, -1, -2.5, -3), tuples.multiply(t, -1)) 37 | self.assertEqual((-1.5, 0, 1.5, 3.75, 4.5), tuples.multiply(t, 1.5)) 38 | self.assertEqual((0,) * 5, tuples.multiply(t, 0)) 39 | self.assertEqual(t, tuples.multiply(t, 1)) 40 | 41 | def test_tuple(self): 42 | t = (-1, 0, 1, 2.5, 3) 43 | self.assertEqual((0, 0, 1.5, -2.5, 9), tuples.multiply(t, (0, 2, 1.5, -1, 3))) 44 | self.assertEqual(t, tuples.multiply(t, (1,) * 5)) 45 | self.assertEqual((0,) * 5, tuples.multiply(t, (0,) * 5)) 46 | 47 | def test_weird_multiplication(self): 48 | t = (-1, 0, 1, 2.5, 3) 49 | self.assertRaises(TypeError, lambda: tuples.multiply(t, 'hello')) 50 | self.assertRaises(TypeError, lambda: tuples.multiply(t, None)) 51 | 52 | def test_too_short_tuple(self): 53 | t = (-1, 0, 1, 2.5, 3) 54 | self.assertRaises(IndexError, lambda: tuples.multiply(t, ())) 55 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from qreader.utils import is_rect_overlapping, is_range_overlapping 2 | from tests.helpers import TestCase 3 | 4 | __author__ = 'ewino' 5 | 6 | 7 | class TestOverlap(TestCase): 8 | def test_overlapping_ranges(self): 9 | # partial overlap 10 | self.assertTrue(is_range_overlapping((1, 3), (2, 4))) 11 | self.assertTrue(is_range_overlapping((-8, -2), (-5, 4))) 12 | # a contains b 13 | self.assertTrue(is_range_overlapping((1, 5), (2, 4))) 14 | # b contains a 15 | self.assertTrue(is_range_overlapping((1, 5), (0, 6))) 16 | 17 | def test_non_overlapping_ranges(self): 18 | # negatives 19 | self.assertFalse(is_range_overlapping((-8, -6), (-4, -2))) 20 | self.assertFalse(is_range_overlapping((-4, -2), (-8, -6))) 21 | # positives 22 | self.assertFalse(is_range_overlapping((8, 6), (4, 2))) 23 | self.assertFalse(is_range_overlapping((4, 2), (8, 6))) 24 | # both 25 | self.assertFalse(is_range_overlapping((-8, -2), (2, 8))) 26 | self.assertFalse(is_range_overlapping((-1, 1), (2, 3))) 27 | 28 | def test_overlapping_zones(self): 29 | # [-----[-]----] 30 | # [ [ ] ] 31 | # [_____[_]____] 32 | self.assertTrue(is_rect_overlapping((1, 1, 4, 4), (3, 1, 6, 4))) 33 | # [-------] 34 | # [ [-]----] 35 | # [_____[_] ] 36 | # [______] 37 | self.assertTrue(is_rect_overlapping((1, 1, 4, 4), (3, 3, 6, 6))) 38 | # [-------] 39 | # [-------] 40 | # [_______] 41 | # [_______] 42 | self.assertTrue(is_rect_overlapping((1, 1, 4, 4), (1, 3, 4, 6))) 43 | # a contains b 44 | self.assertTrue(is_rect_overlapping((1, 1, 4, 4), (2, 2, 4, 4))) 45 | self.assertTrue(is_rect_overlapping((1, 1, 4, 4), (2, 2, 3, 3))) 46 | # b contains a 47 | self.assertTrue(is_rect_overlapping((2, 2, 4, 4), (1, 1, 4, 4))) 48 | self.assertTrue(is_rect_overlapping((2, 2, 3, 3), (1, 1, 4, 4))) 49 | # point rect 50 | self.assertTrue(is_rect_overlapping((2, 2, 4, 4), (4, 4, 4, 4))) 51 | 52 | def test_non_overlapping_zones(self): 53 | # [-----] [-----] 54 | # [ ] [ ] 55 | # [_____] [_____] 56 | self.assertFalse(is_rect_overlapping((1, 1, 4, 4), (5, 1, 8, 4))) 57 | # [--] 58 | # [__] 59 | # [--] 60 | # [__] 61 | self.assertFalse(is_rect_overlapping((1, 1, 4, 4), (5, 5, 8, 8))) 62 | # [-------] 63 | # [_______] 64 | # 65 | # [-------] 66 | # [_______] 67 | self.assertFalse(is_rect_overlapping((1, 1, 4, 4), (1, 5, 4, 8))) 68 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | from qreader.exceptions import QrCorruptError 2 | from qreader.validation import validate_format_info, format_info_check 3 | from tests.helpers import TestCase 4 | 5 | __author__ = 'ewino' 6 | 7 | 8 | class TestFormatInfoValidation(TestCase): 9 | def test_valid_format_info(self): 10 | samples = [ 11 | 0b010001111010110, # L0 12 | 0b011001000111101, # L4 13 | 0b000111101011001, # M3 14 | 0b001101110000101, # M6 15 | 0b110101100100011, # Q2 16 | 0b111010110010001, # Q5 17 | 0b100011110101100, # H1 18 | 0b101110000101001, # H7 19 | ] 20 | for sample in samples: 21 | self.assertEqual(0, format_info_check(sample)) 22 | 23 | def test_invalid_format_info(self): 24 | single_bit_error = [ 25 | 0b010011111010110, # L0 26 | 0b011001010111101, # L4 27 | 0b000111001011001, # M3 28 | 0b001100110000101, # M6 29 | ] 30 | double_bit_error = [ 31 | 0b110101101000011, # Q2 32 | 0b111110110011001, # Q5 33 | 0b101011110001100, # H1 34 | 0b111110000111001, # H7 35 | ] 36 | triple_bit_error = [ 37 | 0b110101101000010, # Q2 38 | 0b111110110011000, # Q5 39 | 0b101011110001101, # H1 40 | 0b111110000111000, # H7 41 | ] 42 | 43 | for sample in single_bit_error + double_bit_error + triple_bit_error: 44 | self.assertNotEqual(0, format_info_check(sample)) 45 | 46 | 47 | class TestFormatInfoErrorCorrection(TestCase): 48 | def test_validate_format_info(self): 49 | samples = [ 50 | 0b010001111010110, # L0 51 | 0b011001000111101, # L4 52 | 0b000111101011001, # M3 53 | 0b001101110000101, # M6 54 | 0b110101100100011, # Q2 55 | 0b111010110010001, # Q5 56 | 0b100011110101100, # H1 57 | 0b101110000101001, # H7 58 | ] 59 | 60 | for sample in samples: 61 | self.assertEqual(sample >> 10, validate_format_info(sample)) 62 | 63 | def test_validate_erroneous_format_info(self): 64 | single_bit_error = [ 65 | (0b010011111010110, 0b01000), # L0 66 | (0b011001010111101, 0b01100), # L4 67 | (0b000111001011001, 0b00011), # M3 68 | (0b001100110000101, 0b00110), # M6 69 | ] 70 | double_bit_error = [ 71 | (0b110101101000011, 0b11010), # Q2 72 | (0b111110110011001, 0b11101), # Q5 73 | (0b101011110001100, 0b10001), # H1 74 | (0b111110000111001, 0b10111), # H7 75 | ] 76 | triple_bit_error = [ 77 | (0b110101101000010, 0b11010), # Q2 78 | (0b111110110011000, 0b11101), # Q5 79 | (0b101011110001101, 0b10001), # H1 80 | (0b111110000111000, 0b10111), # H7 81 | ] 82 | 83 | for sample, correct in single_bit_error + double_bit_error + triple_bit_error: 84 | self.assertEqual(correct, validate_format_info(sample)) 85 | 86 | def test_completely_wrong_format_info(self): 87 | samples = [ 88 | 0b010001111010110, # L0 89 | 0b011001000111101, # L4 90 | 0b000111101011001, # M3 91 | 0b001101110000101, # M6 92 | 0b110101100100011, # Q2 93 | 0b111010110010001, # Q5 94 | 0b100011110101100, # H1 95 | 0b101110000101001, # H7 96 | ] 97 | for sample in samples: 98 | self.assertNotEqual(sample >> 10, validate_format_info(sample ^ 0b111111000000000)) 99 | 100 | def test_unsalvageable_format_info(self): 101 | sample = 0b010001111010110 ^ 0b000001111111111 # flip all error correction bits 102 | self.assertRaises(QrCorruptError, validate_format_info, sample) 103 | 104 | def test_dual_formats_one_right_one_wrong(self): 105 | samples = [ 106 | 0b010001111010110, # L0 107 | 0b011001000111101, # L4 108 | 0b000111101011001, # M3 109 | 0b001101110000101, # M6 110 | 0b110101100100011, # Q2 111 | 0b111010110010001, # Q5 112 | 0b100011110101100, # H1 113 | 0b101110000101001, # H7 114 | ] 115 | for sample in samples: 116 | self.assertEqual(sample >> 10, validate_format_info(sample, sample)) 117 | self.assertEqual(sample >> 10, validate_format_info(sample ^ 0b111100000000000, sample ^ 0b000100000011101)) 118 | -------------------------------------------------------------------------------- /tests/test_vcard.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from dateutil.tz import tzutc 4 | 5 | from qreader.vcard import vCard 6 | from tests.helpers import TestCase 7 | 8 | # The things we do to increase test coverage... 9 | # and it's a temporary module too! 10 | 11 | __author__ = 'ewino' 12 | 13 | 14 | class TestVCardFromText(TestCase): 15 | 16 | def test_format(self): 17 | self.assertTrue(vCard(), vCard.from_text('BEGIN:VCARD\nEND:VCARD')) 18 | self.assertTrue(vCard(), vCard.from_text('BEGIN:VCARD\n\nEND:VCARD')) 19 | self.assertRaises(ValueError, lambda: vCard.from_text('BEGIN:VCARD\nEND:ICAL')) 20 | self.assertRaises(ValueError, lambda: vCard.from_text('Welcome to Jamaica, Have a good day')) 21 | 22 | def test_weird_field(self): 23 | field_name, field_val = 'EWINO', 'BLA' 24 | self.assertRaisesMsg(TypeError, vCard.from_text, 25 | ('Unknown vCard field: {0}. ' 26 | 'This implementation only supports basic properties').format(field_name), 27 | 'BEGIN:VCARD\n{0}:{1}\nEND:VCARD'.format(field_name, field_val)) 28 | vCard.from_text('BEGIN:VCARD\nVERSION:4\nEND:VCARD') 29 | 30 | def test_date_fields(self): 31 | self.assertEqual(datetime(2015, 8, 28, tzinfo=tzutc()), 32 | vCard.from_text('BEGIN:VCARD\nANNIVERSARY:20150828T000000Z\nEND:VCARD').anniversary) 33 | 34 | def test_complex_fields(self): 35 | card = vCard.from_text('BEGIN:VCARD\n' 36 | 'TEL;TYPE=WORK,VOICE:(111) 555-1212\n' 37 | 'ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America\n' 38 | 'CATEGORIES:swimmer,biker\n' 39 | 'END:VCARD') 40 | self.assertEqual(1, len(card.phones)) 41 | self.assertEqual(('WORK,VOICE', '(111) 555-1212'), card.phones[0]) 42 | self.assertEqual(1, len(card.addresses)) 43 | self.assertEqual(('WORK', '', '', '100 Waters Edge', 'Baytown', 'LA', '30314', 'United States of America'), 44 | card.addresses[0]) 45 | self.assertEqual(2, len(card.categories)) 46 | self.assertEqual(['swimmer', 'biker'], card.categories) 47 | --------------------------------------------------------------------------------