codekingpro/portable-devtools
114k
1import sys2from bisect import bisect_left3from typing import (4 Dict,5 Generic,6 List,7 NamedTuple,8 Optional,9 Type,10 TypeVar,11 cast,12 overload,13)14 15from typing_extensions import Literal16 17from qrcode import constants, exceptions, util18from qrcode.image.base import BaseImage19from qrcode.image.pure import PyPNGImage20 21ModulesType = List[List[Optional[bool]]]22# Cache modules generated just based on the QR Code version23precomputed_qr_blanks: Dict[int, ModulesType] = {}24 25 26def make(data=None, **kwargs):27 qr = QRCode(**kwargs)28 qr.add_data(data)29 return qr.make_image()30 31 32def _check_box_size(size):33 if int(size) <= 0:34 raise ValueError(f"Invalid box size (was {size}, expected larger than 0)")35 36 37def _check_border(size):38 if int(size) < 0:39 raise ValueError(40 "Invalid border value (was %s, expected 0 or larger than that)" % size41 )42 43 44def _check_mask_pattern(mask_pattern):45 if mask_pattern is None:46 return47 if not isinstance(mask_pattern, int):48 raise TypeError(49 f"Invalid mask pattern (was {type(mask_pattern)}, expected int)"50 )51 if mask_pattern < 0 or mask_pattern > 7:52 raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})")53 54 55def copy_2d_array(x):56 return [row[:] for row in x]57 58 59class ActiveWithNeighbors(NamedTuple):60 NW: bool61 N: bool62 NE: bool63 W: bool64 me: bool65 E: bool66 SW: bool67 S: bool68 SE: bool69 70 def __bool__(self) -> bool:71 return self.me72 73 74GenericImage = TypeVar("GenericImage", bound=BaseImage)75GenericImageLocal = TypeVar("GenericImageLocal", bound=BaseImage)76 77 78class QRCode(Generic[GenericImage]):79 modules: ModulesType80 _version: Optional[int] = None81 82 def __init__(83 self,84 version=None,85 error_correction=constants.ERROR_CORRECT_M,86 box_size=10,87 border=4,88 image_factory: Optional[Type[GenericImage]] = None,89 mask_pattern=None,90 ):91 _check_box_size(box_size)92 _check_border(border)93 self.version = version94 self.error_correction = int(error_correction)95 self.box_size = int(box_size)96 # Spec says border should be at least four boxes wide, but allow for97 # any (e.g. for producing printable QR codes).98 self.border = int(border)99 self.mask_pattern = mask_pattern100 self.image_factory = image_factory101 if image_factory is not None:102 assert issubclass(image_factory, BaseImage)103 self.clear()104 105 @property106 def version(self) -> int:107 if self._version is None:108 self.best_fit()109 return cast(int, self._version)110 111 @version.setter112 def version(self, value) -> None:113 if value is not None:114 value = int(value)115 util.check_version(value)116 self._version = value117 118 @property119 def mask_pattern(self):120 return self._mask_pattern121 122 @mask_pattern.setter123 def mask_pattern(self, pattern):124 _check_mask_pattern(pattern)125 self._mask_pattern = pattern126 127 def clear(self):128 """129 Reset the internal data.130 """131 self.modules = [[]]132 self.modules_count = 0133 self.data_cache = None134 self.data_list = []135 136 def add_data(self, data, optimize=20):137 """138 Add data to this QR Code.139 140 :param optimize: Data will be split into multiple chunks to optimize141 the QR size by finding to more compressed modes of at least this142 length. Set to ``0`` to avoid optimizing at all.143 """144 if isinstance(data, util.QRData):145 self.data_list.append(data)146 elif optimize:147 self.data_list.extend(util.optimal_data_chunks(data, minimum=optimize))148 else:149 self.data_list.append(util.QRData(data))150 self.data_cache = None151 152 def make(self, fit=True):153 """154 Compile the data into a QR Code array.155 156 :param fit: If ``True`` (or if a size has not been provided), find the157 best fit for the data to avoid data overflow errors.158 """159 if fit or (self.version is None):160 self.best_fit(start=self.version)161 if self.mask_pattern is None:162 self.makeImpl(False, self.best_mask_pattern())163 else:164 self.makeImpl(False, self.mask_pattern)165 166 def makeImpl(self, test, mask_pattern):167 self.modules_count = self.version * 4 + 17168 169 if self.version in precomputed_qr_blanks:170 self.modules = copy_2d_array(precomputed_qr_blanks[self.version])171 else:172 self.modules = [173 [None] * self.modules_count for i in range(self.modules_count)174 ]175 self.setup_position_probe_pattern(0, 0)176 self.setup_position_probe_pattern(self.modules_count - 7, 0)177 self.setup_position_probe_pattern(0, self.modules_count - 7)178 self.setup_position_adjust_pattern()179 self.setup_timing_pattern()180 181 precomputed_qr_blanks[self.version] = copy_2d_array(self.modules)182 183 self.setup_type_info(test, mask_pattern)184 185 if self.version >= 7:186 self.setup_type_number(test)187 188 if self.data_cache is None:189 self.data_cache = util.create_data(190 self.version, self.error_correction, self.data_list191 )192 self.map_data(self.data_cache, mask_pattern)193 194 def setup_position_probe_pattern(self, row, col):195 for r in range(-1, 8):196 197 if row + r <= -1 or self.modules_count <= row + r:198 continue199 200 for c in range(-1, 8):201 202 if col + c <= -1 or self.modules_count <= col + c:203 continue204 205 if (206 (0 <= r <= 6 and c in {0, 6})207 or (0 <= c <= 6 and r in {0, 6})208 or (2 <= r <= 4 and 2 <= c <= 4)209 ):210 self.modules[row + r][col + c] = True211 else:212 self.modules[row + r][col + c] = False213 214 def best_fit(self, start=None):215 """216 Find the minimum size required to fit in the data.217 """218 if start is None:219 start = 1220 util.check_version(start)221 222 # Corresponds to the code in util.create_data, except we don't yet know223 # version, so optimistically assume start and check later224 mode_sizes = util.mode_sizes_for_version(start)225 buffer = util.BitBuffer()226 for data in self.data_list:227 buffer.put(data.mode, 4)228 buffer.put(len(data), mode_sizes[data.mode])229 data.write(buffer)230 231 needed_bits = len(buffer)232 self.version = bisect_left(233 util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start234 )235 if self.version == 41:236 raise exceptions.DataOverflowError()237 238 # Now check whether we need more bits for the mode sizes, recursing if239 # our guess was too low240 if mode_sizes is not util.mode_sizes_for_version(self.version):241 self.best_fit(start=self.version)242 return self.version243 244 def best_mask_pattern(self):245 """246 Find the most efficient mask pattern.247 """248 min_lost_point = 0249 pattern = 0250 251 for i in range(8):252 self.makeImpl(True, i)253 254 lost_point = util.lost_point(self.modules)255 256 if i == 0 or min_lost_point > lost_point:257 min_lost_point = lost_point258 pattern = i259 260 return pattern261 262 def print_tty(self, out=None):263 """264 Output the QR Code only using TTY colors.265 266 If the data has not been compiled yet, make it first.267 """268 if out is None:269 import sys270 271 out = sys.stdout272 273 if not out.isatty():274 raise OSError("Not a tty")275 276 if self.data_cache is None:277 self.make()278 279 modcount = self.modules_count280 out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")281 for r in range(modcount):282 out.write("\x1b[1;47m \x1b[40m")283 for c in range(modcount):284 if self.modules[r][c]:285 out.write(" ")286 else:287 out.write("\x1b[1;47m \x1b[40m")288 out.write("\x1b[1;47m \x1b[0m\n")289 out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")290 out.flush()291 292 def print_ascii(self, out=None, tty=False, invert=False):293 """294 Output the QR Code using ASCII characters.295 296 :param tty: use fixed TTY color codes (forces invert=True)297 :param invert: invert the ASCII characters (solid <-> transparent)298 """299 if out is None:300 out = sys.stdout301 302 if tty and not out.isatty():303 raise OSError("Not a tty")304 305 if self.data_cache is None:306 self.make()307 308 modcount = self.modules_count309 codes = [bytes((code,)).decode("cp437") for code in (255, 223, 220, 219)]310 if tty:311 invert = True312 if invert:313 codes.reverse()314 315 def get_module(x, y) -> int:316 if invert and self.border and max(x, y) >= modcount + self.border:317 return 1318 if min(x, y) < 0 or max(x, y) >= modcount:319 return 0320 return cast(int, self.modules[x][y])321 322 for r in range(-self.border, modcount + self.border, 2):323 if tty:324 if not invert or r < modcount + self.border - 1:325 out.write("\x1b[48;5;232m") # Background black326 out.write("\x1b[38;5;255m") # Foreground white327 for c in range(-self.border, modcount + self.border):328 pos = get_module(r, c) + (get_module(r + 1, c) << 1)329 out.write(codes[pos])330 if tty:331 out.write("\x1b[0m")332 out.write("\n")333 out.flush()334 335 @overload336 def make_image(self, image_factory: Literal[None] = None, **kwargs) -> GenericImage:337 ...338 339 @overload340 def make_image(341 self, image_factory: Type[GenericImageLocal] = None, **kwargs342 ) -> GenericImageLocal:343 ...344 345 def make_image(self, image_factory=None, **kwargs):346 """347 Make an image from the QR Code data.348 349 If the data has not been compiled yet, make it first.350 """351 _check_box_size(self.box_size)352 if self.data_cache is None:353 self.make()354 355 if image_factory is not None:356 assert issubclass(image_factory, BaseImage)357 else:358 image_factory = self.image_factory359 if image_factory is None:360 from qrcode.image.pil import Image, PilImage361 362 # Use PIL by default if available, otherwise use PyPNG.363 image_factory = PilImage if Image else PyPNGImage364 365 im = image_factory(366 self.border,367 self.modules_count,368 self.box_size,369 qrcode_modules=self.modules,370 **kwargs,371 )372 373 if im.needs_drawrect:374 for r in range(self.modules_count):375 for c in range(self.modules_count):376 if im.needs_context:377 im.drawrect_context(r, c, qr=self)378 elif self.modules[r][c]:379 im.drawrect(r, c)380 if im.needs_processing:381 im.process()382 383 return im384 385 # return true if and only if (row, col) is in the module386 def is_constrained(self, row: int, col: int) -> bool:387 return (388 row >= 0389 and row < len(self.modules)390 and col >= 0391 and col < len(self.modules[row])392 )393 394 def setup_timing_pattern(self):395 for r in range(8, self.modules_count - 8):396 if self.modules[r][6] is not None:397 continue398 self.modules[r][6] = r % 2 == 0399 400 for c in range(8, self.modules_count - 8):401 if self.modules[6][c] is not None:402 continue403 self.modules[6][c] = c % 2 == 0404 405 def setup_position_adjust_pattern(self):406 pos = util.pattern_position(self.version)407 408 for i in range(len(pos)):409 410 row = pos[i]411 412 for j in range(len(pos)):413 414 col = pos[j]415 416 if self.modules[row][col] is not None:417 continue418 419 for r in range(-2, 3):420 421 for c in range(-2, 3):422 423 if (424 r == -2425 or r == 2426 or c == -2427 or c == 2428 or (r == 0 and c == 0)429 ):430 self.modules[row + r][col + c] = True431 else:432 self.modules[row + r][col + c] = False433 434 def setup_type_number(self, test):435 bits = util.BCH_type_number(self.version)436 437 for i in range(18):438 mod = not test and ((bits >> i) & 1) == 1439 self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod440 441 for i in range(18):442 mod = not test and ((bits >> i) & 1) == 1443 self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod444 445 def setup_type_info(self, test, mask_pattern):446 data = (self.error_correction << 3) | mask_pattern447 bits = util.BCH_type_info(data)448 449 # vertical450 for i in range(15):451 452 mod = not test and ((bits >> i) & 1) == 1453 454 if i < 6:455 self.modules[i][8] = mod456 elif i < 8:457 self.modules[i + 1][8] = mod458 else:459 self.modules[self.modules_count - 15 + i][8] = mod460 461 # horizontal462 for i in range(15):463 464 mod = not test and ((bits >> i) & 1) == 1465 466 if i < 8:467 self.modules[8][self.modules_count - i - 1] = mod468 elif i < 9:469 self.modules[8][15 - i - 1 + 1] = mod470 else:471 self.modules[8][15 - i - 1] = mod472 473 # fixed module474 self.modules[self.modules_count - 8][8] = not test475 476 def map_data(self, data, mask_pattern):477 inc = -1478 row = self.modules_count - 1479 bitIndex = 7480 byteIndex = 0481 482 mask_func = util.mask_func(mask_pattern)483 484 data_len = len(data)485 486 for col in range(self.modules_count - 1, 0, -2):487 488 if col <= 6:489 col -= 1490 491 col_range = (col, col - 1)492 493 while True:494 495 for c in col_range:496 497 if self.modules[row][c] is None:498 499 dark = False500 501 if byteIndex < data_len:502 dark = ((data[byteIndex] >> bitIndex) & 1) == 1503 504 if mask_func(row, c):505 dark = not dark506 507 self.modules[row][c] = dark508 bitIndex -= 1509 510 if bitIndex == -1:511 byteIndex += 1512 bitIndex = 7513 514 row += inc515 516 if row < 0 or self.modules_count <= row:517 row -= inc518 inc = -inc519 break520 521 def get_matrix(self):522 """523 Return the QR Code as a multidimensional array, including the border.524 525 To return the array without a border, set ``self.border`` to 0 first.526 """527 if self.data_cache is None:528 self.make()529 530 if not self.border:531 return self.modules532 533 width = len(self.modules) + self.border * 2534 code = [[False] * width] * self.border535 x_border = [False] * self.border536 for module in self.modules:537 code.append(x_border + cast(List[bool], module) + x_border)538 code += [[False] * width] * self.border539 540 return code541 542 def active_with_neighbors(self, row: int, col: int) -> ActiveWithNeighbors:543 context: List[bool] = []544 for r in range(row - 1, row + 2):545 for c in range(col - 1, col + 2):546 context.append(self.is_constrained(r, c) and bool(self.modules[r][c]))547 return ActiveWithNeighbors(*context)548 