#!/usr/bin/env python3 # coding=utf-8 # # Copyright (C) 2009 Kazuhiko Arase (http://www.d-project.com/) # 2010 Bulia Byak # 2018 Kirill Okhotnikov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA. # """ Provide the QR Code rendering. """ from __future__ import print_function from itertools import product from collections import defaultdict import numpy as np import inkex from inkex import Group, Rectangle, Use, PathElement from inkex.paths import Move, zoneClose, Line, Curve from inkex.localization import inkex_gettext as _ from inkex.utils import circular_pairwise class QRLengthError(Exception): def __init__(self, message): self.message = message class QRCode(object): PAD0 = 0xEC PAD1 = 0x11 def __init__(self, correction): self.typeNumber = 1 self.errorCorrectLevel = correction self.qrDataList = [] self.modules = [] self.moduleCount = 0 def getTypeNumber(self): return self.typeNumber def setTypeNumber(self, typeNumber): self.typeNumber = typeNumber def clearData(self): self.qrDataList = [] def addData(self, data): self.qrDataList.append(data) def getDataCount(self): return len(self.qrDataList) def getData(self, index): return self.qrDataList[index] def isDark(self, row, col): return self.modules[row][col] if self.modules[row][col] is not None else False def getModuleCount(self): return self.moduleCount def make(self): self._make(False, self._getBestMaskPattern()) def _getBestMaskPattern(self): minLostPoint = 0 pattern = 0 for i in range(8): self._make(True, i) lostPoint = QRUtil.getLostPoint(self) if i == 0 or minLostPoint > lostPoint: minLostPoint = lostPoint pattern = i return pattern def _make(self, test, maskPattern): self.moduleCount = self.typeNumber * 4 + 17 self.modules = np.array( [[None] * self.moduleCount for i in range(self.moduleCount)] ) self._setupPositionProbePattern(0, 0) self._setupPositionProbePattern(self.moduleCount - 7, 0) self._setupPositionProbePattern(0, self.moduleCount - 7) self._setupPositionAdjustPattern() self._setupTimingPattern() self._setupTypeInfo(test, maskPattern) if self.typeNumber >= 7: self._setupTypeNumber(test) data = QRCode._createData( self.typeNumber, self.errorCorrectLevel, self.qrDataList ) self._mapData(data, maskPattern) def _mapData(self, data, maskPattern): rows = list(range(self.moduleCount)) cols = [ col - 1 if col <= 6 else col for col in range(self.moduleCount - 1, 0, -2) ] maskFunc = QRUtil.getMaskFunction(maskPattern) byteIndex = 0 bitIndex = 7 for col in cols: rows.reverse() for row in rows: for c in range(2): if self.modules[row][col - c] is None: dark = False if byteIndex < len(data): dark = ((data[byteIndex] >> bitIndex) & 1) == 1 if maskFunc(row, col - c): dark = not dark self.modules[row][col - c] = dark bitIndex -= 1 if bitIndex == -1: byteIndex += 1 bitIndex = 7 def _setupPositionAdjustPattern(self): pos = QRUtil.getPatternPosition(self.typeNumber) for row in pos: for col in pos: if self.modules[row][col] is not None: continue for r in range(-2, 3): for c in range(-2, 3): self.modules[row + r][col + c] = ( r == -2 or r == 2 or c == -2 or c == 2 or (r == 0 and c == 0) ) def _setupPositionProbePattern(self, row, col): for r in range(-1, 8): for c in range(-1, 8): if ( row + r <= -1 or self.moduleCount <= row + r or col + c <= -1 or self.moduleCount <= col + c ): continue self.modules[row + r][col + c] = ( (0 <= r <= 6 and (c == 0 or c == 6)) or (0 <= c <= 6 and (r == 0 or r == 6)) or (2 <= r <= 4 and 2 <= c <= 4) ) def _setupTimingPattern(self): for r in range(8, self.moduleCount - 8): if self.modules[r][6] is not None: continue self.modules[r][6] = r % 2 == 0 for c in range(8, self.moduleCount - 8): if self.modules[6][c] is not None: continue self.modules[6][c] = c % 2 == 0 def _setupTypeNumber(self, test): bits = QRUtil.getBCHTypeNumber(self.typeNumber) for i in range(18): self.modules[i // 3][i % 3 + self.moduleCount - 8 - 3] = ( not test and ((bits >> i) & 1) == 1 ) for i in range(18): self.modules[i % 3 + self.moduleCount - 8 - 3][i // 3] = ( not test and ((bits >> i) & 1) == 1 ) def _setupTypeInfo(self, test, maskPattern): data = (self.errorCorrectLevel << 3) | maskPattern bits = QRUtil.getBCHTypeInfo(data) # vertical for i in range(15): mod = not test and ((bits >> i) & 1) == 1 if i < 6: self.modules[i][8] = mod elif i < 8: self.modules[i + 1][8] = mod else: self.modules[self.moduleCount - 15 + i][8] = mod # horizontal for i in range(15): mod = not test and ((bits >> i) & 1) == 1 if i < 8: self.modules[8][self.moduleCount - i - 1] = mod elif i < 9: self.modules[8][15 - i - 1 + 1] = mod else: self.modules[8][15 - i - 1] = mod # fixed self.modules[self.moduleCount - 8][8] = not test @staticmethod def _createData(typeNumber, errorCorrectLevel, dataArray): rsBlocks = RSBlock.getRSBlocks(typeNumber, errorCorrectLevel) buffer = BitBuffer() for data in dataArray: buffer.put(data.getMode(), 4) buffer.put(data.getLength(), data.getLengthInBits(typeNumber)) data.write(buffer) totalDataCount = sum(rsBlock.getDataCount() for rsBlock in rsBlocks) if buffer.getLengthInBits() > totalDataCount * 8: raise QRLengthError( "code length overflow. (%s>%s)" % (buffer.getLengthInBits(), totalDataCount * 8) ) # end code if buffer.getLengthInBits() + 4 <= totalDataCount * 8: buffer.put(0, 4) # padding while buffer.getLengthInBits() % 8 != 0: buffer.put(False, 1) # padding while True: if buffer.getLengthInBits() >= totalDataCount * 8: break buffer.put(QRCode.PAD0, 8) if buffer.getLengthInBits() >= totalDataCount * 8: break buffer.put(QRCode.PAD1, 8) return QRCode._createBytes(buffer, rsBlocks) @staticmethod def _createBytes(buffer, rsBlocks): offset = 0 maxDcCount = 0 maxEcCount = 0 dcdata = [None] * len(rsBlocks) ecdata = [None] * len(rsBlocks) for r, b in enumerate(rsBlocks): dcCount = b.getDataCount() ecCount = b.getTotalCount() - dcCount maxDcCount = max(maxDcCount, dcCount) maxEcCount = max(maxEcCount, ecCount) dcdata[r] = [0] * dcCount for i in range(len(dcdata[r])): dcdata[r][i] = 0xFF & buffer.getBuffer()[i + offset] offset += dcCount rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount) rawPoly = Polynomial(dcdata[r], rsPoly.getLength() - 1) modPoly = rawPoly.mod(rsPoly) ecdata[r] = [0] * (rsPoly.getLength() - 1) for i in range(len(ecdata[r])): modIndex = i + modPoly.getLength() - len(ecdata[r]) ecdata[r][i] = modPoly.get(modIndex) if modIndex >= 0 else 0 totalCodeCount = sum(rsBlock.getTotalCount() for rsBlock in rsBlocks) data = [0] * totalCodeCount index = 0 for i in range(maxDcCount): for r in range(len(rsBlocks)): if i < len(dcdata[r]): data[index] = dcdata[r][i] index += 1 for i in range(maxEcCount): for r in range(len(rsBlocks)): if i < len(ecdata[r]): data[index] = ecdata[r][i] index += 1 return data @staticmethod def getMinimumQRCode(data, errorCorrectLevel): qr = QRCode(correction=errorCorrectLevel) qr.addData(data) lv = 1 rv = 40 while rv - lv > 0: mid = (3 * lv + rv) // 4 qr.setTypeNumber(mid) try: qr.make() except QRLengthError: if mid == 40: raise inkex.AbortExtension( _("The string is too large to represent as QR code") ) lv = mid + 1 else: rv = mid qr.setTypeNumber(rv) qr.make() return qr class Mode(object): MODE_NUMBER = 1 << 0 MODE_ALPHA_NUM = 1 << 1 MODE_8BIT_BYTE = 1 << 2 MODE_KANJI = 1 << 3 class MaskPattern(object): PATTERN000 = 0 PATTERN001 = 1 PATTERN010 = 2 PATTERN011 = 3 PATTERN100 = 4 PATTERN101 = 5 PATTERN110 = 6 PATTERN111 = 7 class QRUtil(object): @staticmethod def getPatternPosition(typeNumber): return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1] PATTERN_POSITION_TABLE = [ [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170], ] @staticmethod def getErrorCorrectPolynomial(errorCorrectLength): a = Polynomial([1]) for i in range(errorCorrectLength): a = a.multiply(Polynomial([1, QRMath.gexp(i)])) return a @staticmethod def getMaskFunction(maskPattern): return { MaskPattern.PATTERN000: lambda i, j: (i + j) % 2 == 0, MaskPattern.PATTERN001: lambda i, j: i % 2 == 0, MaskPattern.PATTERN010: lambda i, j: j % 3 == 0, MaskPattern.PATTERN011: lambda i, j: (i + j) % 3 == 0, MaskPattern.PATTERN100: lambda i, j: (i // 2 + j // 3) % 2 == 0, MaskPattern.PATTERN101: lambda i, j: (i * j) % 2 + (i * j) % 3 == 0, MaskPattern.PATTERN110: lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0, MaskPattern.PATTERN111: lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0, }[maskPattern] @staticmethod def getLostPoint(qrcode: QRCode): moduleCount = qrcode.getModuleCount() lostPoint = 0 other = qrcode.modules.astype("int") # LEVEL1 same = np.zeros_like(other) same[:-1, :] += other[:-1, :] == other[1:, :] # east same[:-1, :-1] += other[:-1, :-1] == other[1:, 1:] # north east same[:, :-1] += other[:, :-1] == other[:, 1:] # north same[1:, :-1] += other[1:, :-1] == other[:-1, 1:] # north west same[1:, :] += other[1:, :] == other[:-1, :] # west same[1:, 1:] += other[1:, 1:] == other[:-1, :-1] # south west same[:, 1:] += other[:, 1:] == other[:, :-1] # south same[:-1, 1:] += other[:-1, 1:] == other[1:, :-1] # south east lostPoint = np.sum(np.where(same > 5, 3 + same - 5, 0)) # LEVEL2 lostPoint += 3 * np.sum( np.mod(other[:-1, :-1] + other[1:, :-1] + other[:-1, 1:] + other[1:, 1:], 4) == 0 ) # LEVEL3 lostPoint += 40 * np.sum( (other[:, 0:-6] == 1) & (other[:, 1:-5] == 0) & (other[:, 2:-4] == 1) & (other[:, 3:-3] == 1) & (other[:, 4:-2] == 1) & (other[:, 5:-1] == 0) & (other[:, 6:] == 1) ) lostPoint += 40 * np.sum( (other[0:-6] == 1) & (other[1:-5, :] == 0) & (other[2:-4, :] == 1) & (other[3:-3, :] == 1) & (other[4:-2, :] == 1) & (other[5:-1, :] == 0) & (other[6:, :] == 1) ) # LEVEL4 darkCount = np.sum(other) ratio = abs(100 * darkCount // moduleCount // moduleCount - 50) // 5 lostPoint += ratio * 10 return lostPoint G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0) G18 = ( (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0) ) G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1) @staticmethod def getBCHTypeInfo(data): d = data << 10 while QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0: d ^= QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15)) return ((data << 10) | d) ^ QRUtil.G15_MASK @staticmethod def getBCHTypeNumber(data): d = data << 12 while QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0: d ^= QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18)) return (data << 12) | d @staticmethod def getBCHDigit(data): digit = 0 while data != 0: digit += 1 data >>= 1 return digit class QRData(object): def __init__(self, mode, data): self._mode = mode self.data = data def getMode(self): return self._mode def getData(self): return self.data def getLength(self): return len(self.data) # @abstractmethod def write(self, buffer): pass def getLengthInBits(self, type): if 1 <= type < 10: # 1 - 9 return { Mode.MODE_NUMBER: 10, Mode.MODE_ALPHA_NUM: 9, Mode.MODE_8BIT_BYTE: 8, Mode.MODE_KANJI: 8, }[self._mode] elif type < 27: # 10 - 26 return { Mode.MODE_NUMBER: 12, Mode.MODE_ALPHA_NUM: 11, Mode.MODE_8BIT_BYTE: 16, Mode.MODE_KANJI: 10, }[self._mode] elif type < 41: # 27 - 40 return { Mode.MODE_NUMBER: 14, Mode.MODE_ALPHA_NUM: 13, Mode.MODE_8BIT_BYTE: 16, Mode.MODE_KANJI: 12, }[self._mode] else: raise Exception("type:%s" % type) class QR8BitByte(QRData): def __init__(self, data): super(QR8BitByte, self).__init__(Mode.MODE_8BIT_BYTE, data) if isinstance(data, str): data = data.encode("ascii", "ignore") if not isinstance(data, bytes): raise ValueError("Data must be in bytes!") def write(self, buffer): for d in self.data: buffer.put(ord(d), 8) class QRAlphaNum(QRData): def __init__(self, data): super(QRAlphaNum, self).__init__(Mode.MODE_ALPHA_NUM, data) def write(self, buffer): i = 0 while i + 1 < len(self.data): buffer.put( QRAlphaNum._getCode(self.data[i]) * 45 + QRAlphaNum._getCode(self.data[i + 1]), 11, ) i += 2 if i < len(self.data): buffer.put(QRAlphaNum._getCode(self.data[i]), 6) @staticmethod def _getCode(c): if "0" <= c and c <= "9": return ord(c) - ord("0") elif "A" <= c and c <= "Z": return ord(c) - ord("A") + 10 else: dct = { " ": 36, "$": 37, "%": 38, "*": 39, "+": 40, "-": 41, ".": 42, "/": 43, ":": 44, } if c in dct.keys(): return dct[c] else: raise inkex.AbortExtension( _( "Wrong symbol '{}' in alphanumeric representation: Should be [A-Z, 0-9] or {}" ).format(c, dct.keys()) ) class QRNumber(QRData): def __init__(self, data): super(QRNumber, self).__init__(Mode.MODE_NUMBER, data) def write(self, buffer): i = 0 try: while i + 2 < len(self.data): num = int(self.data[i : i + 3]) buffer.put(num, 10) i += 3 if i < len(self.data): ln = 4 if len(self.data) - i == 1 else 7 buffer.put(int(self.data[i:]), ln) except: raise ValueError("The string '{}' is not a valid number".format(self.data)) class QRKanji(QRData): def __init__(self, data): super(QRKanji, self).__init__(Mode.MODE_KANJI, data) raise RuntimeError("Class QRKanji is not implemented") class QRMath(object): EXP_TABLE = None LOG_TABLE = None @staticmethod def _init(): QRMath.EXP_TABLE = [0] * 256 for i in range(256): QRMath.EXP_TABLE[i] = ( 1 << i if i < 8 else QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8] ) QRMath.LOG_TABLE = [0] * 256 for i in range(255): QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i @staticmethod def glog(n): return QRMath.LOG_TABLE[n] @staticmethod def gexp(n): return QRMath.EXP_TABLE[n % 255] # initialize statics QRMath._init() class Polynomial(object): def __init__(self, num, shift=0): offset = 0 length = len(num) while offset < length and num[offset] == 0: offset += 1 self.num = num[offset:] + [0] * shift def get(self, index): return self.num[index] def getLength(self): return len(self.num) def __repr__(self): return ",".join([str(self.get(i)) for i in range(self.getLength())]) def toLogString(self): return ",".join( [str(QRMath.glog(self.get(i))) for i in range(self.getLength())] ) def multiply(self, e): num = [0] * (self.getLength() + e.getLength() - 1) for i in range(self.getLength()): for j in range(e.getLength()): num[i + j] ^= QRMath.gexp( QRMath.glog(self.get(i)) + QRMath.glog(e.get(j)) ) return Polynomial(num) def mod(self, e): if self.getLength() - e.getLength() < 0: return self ratio = QRMath.glog(self.get(0)) - QRMath.glog(e.get(0)) num = self.num[:] for i in range(e.getLength()): num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio) return Polynomial(num).mod(e) class RSBlock(object): RS_BLOCK_TABLE = [ # L # M # Q # H # 1 [1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], # 2 [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], # 3 [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], # 4 [1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], # 5 [1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], # 6 [2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], # 7 [2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], # 8 [2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], # 9 [2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], # 10 [2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], # 11 [4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], # 12 [2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], # 13 [4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], # 14 [3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], # 15 [5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12, 7, 37, 13], # 16 [5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], # 17 [1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], # 18 [5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], # 19 [3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], # 20 [3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], # 21 [4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], # 22 [2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], # 23 [4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], # 24 [6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], # 25 [8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], # 26 [10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], # 27 [8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], # 28 [3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], # 29 [7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], # 30 [5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], # 31 [13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], # 32 [17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], # 33 [17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], # 34 [13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], # 35 [12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], # 36 [6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], # 37 [17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], # 38 [4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], # 39 [20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], # 40 [19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16], ] def __init__(self, totalCount, dataCount): self.totalCount = totalCount self.dataCount = dataCount def getDataCount(self): return self.dataCount def getTotalCount(self): return self.totalCount def __repr__(self): return "(total=%s,data=%s)" % (self.totalCount, self.dataCount) @staticmethod def getRSBlocks(typeNumber, errorCorrectLevel): rsBlock = RSBlock.getRsBlockTable(typeNumber, errorCorrectLevel) length = len(rsBlock) // 3 list = [] for i in range(length): count = rsBlock[i * 3 + 0] totalCount = rsBlock[i * 3 + 1] dataCount = rsBlock[i * 3 + 2] list += [RSBlock(totalCount, dataCount)] * count return list @staticmethod def getRsBlockTable(typeNumber, errorCorrectLevel): return { 1: RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0], 0: RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1], 3: RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2], 2: RSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3], }[errorCorrectLevel] class BitBuffer(object): def __init__(self, inclements=32): self.inclements = inclements self.buffer = [0] * self.inclements self.length = 0 def getBuffer(self): return self.buffer def getLengthInBits(self): return self.length def get(self, index): return ((self.buffer[index // 8] >> (7 - index % 8)) & 1) == 1 def putBit(self, bit): if self.length == len(self.buffer) * 8: self.buffer += [0] * self.inclements if bit: self.buffer[self.length // 8] |= 0x80 >> (self.length % 8) self.length += 1 def put(self, num, length): for i in range(length): self.putBit(((num >> (length - i - 1)) & 1) == 1) def __repr__(self): return "".join( "1" if self.get(i) else "0" for i in range(self.getLengthInBits()) ) class GridDrawer(object): """Mechanism to draw grids of boxes""" def __init__(self, invert_code, smooth_factor): self.invert_code = invert_code self.smoothFactor = smooth_factor self.grid = None self.row_count = 0 self.col_count = 0 def set_grid(self, grid): if len({len(g) for g in grid}) != 1: raise Exception("The array is not rectangular") else: self.grid = grid self.row_count = len(self.grid) self.col_count = len(self.grid[0]) def isDark(self, col, row): inside = 0 <= col < self.col_count and 0 <= row < self.row_count return False if not inside else self.grid[row][col] != self.invert_code dm = {0: (1, 0), 1: (0, -1), 2: (-1, 0), 3: (0, 1)} @staticmethod def moveByDirection(xyd): return xyd[0] + GridDrawer.dm[xyd[2]][0], xyd[1] + GridDrawer.dm[xyd[2]][1] @staticmethod def makeDirectionsTable(): result = [] for cfg in product(range(2), repeat=4): result.append([]) for d in range(4): if cfg[3 - d] == 0 and cfg[3 - (d - 1) % 4] != 0: result[-1].append(d) return result def createVertexesForAdvDrawer(self): dirTable = self.makeDirectionsTable() result = [] # Create vertex for row in range(self.row_count + 1): for col in range(self.col_count + 1): indx = ( (2**0 if self.isDark(col - 0, row - 1) else 0) + (2**1 if self.isDark(col - 1, row - 1) else 0) + (2**2 if self.isDark(col - 1, row - 0) else 0) + (2**3 if self.isDark(col - 0, row - 0) else 0) ) for d in dirTable[indx]: result.append((col, row, d, len(dirTable[indx]) > 1)) return result def getSmoothPosition(self, v, extraSmoothFactor=1.0): vn = self.moveByDirection(v) sc = extraSmoothFactor * self.smoothFactor / 2.0 sc1 = 1.0 - sc return (v[0] * sc1 + vn[0] * sc, v[1] * sc1 + vn[1] * sc), ( v[0] * sc + vn[0] * sc1, v[1] * sc + vn[1] * sc1, ) class QrCode(inkex.GenerateExtension): """Generate QR Code Extension""" def add_arguments(self, pars): pars.add_argument("--text", default="https://inkscape.org") pars.add_argument("--typenumber", type=int, default=0) pars.add_argument("--correctionlevel", type=int, default=0) pars.add_argument("--qrmode", type=int, default=0) pars.add_argument("--encoding", default="latin_1") pars.add_argument("--modulesize", type=float, default=4.0) pars.add_argument("--invert", type=inkex.Boolean, default="false") pars.add_argument( "--drawtype", default="smooth", choices=["smooth", "pathpreset", "selection", "symbol"], ) pars.add_argument( "--smoothness", default="neutral", choices=["neutral", "greedy", "proud"] ) pars.add_argument("--pathtype", default="simple", choices=["simple", "circle"]) pars.add_argument("--smoothval", type=float, default=0.2) pars.add_argument("--symbolid", default="") pars.add_argument("--groupid", default="") def generate(self): scale = self.svg.unittouu("1px") # convert to document units opt = self.options if not opt.text: raise inkex.AbortExtension(_("Please enter an input text")) elif opt.drawtype == "symbol" and opt.symbolid == "": raise inkex.AbortExtension(_("Please enter symbol id")) # for Python 3 ugly hack to represent bytes as str for Python2 compatibility text_str = str(opt.text) text_in = opt.text.replace("\\n", "\n") cmode = [QR8BitByte, QRNumber, QRAlphaNum, QRKanji][opt.qrmode] text_data = cmode(bytes(text_in, opt.encoding).decode("latin_1")) grp = Group() grp.set("inkscape:label", "QR Code: " + text_str) if opt.groupid: grp.set("id", opt.groupid) pos_x, pos_y = self.svg.namedview.center grp.transform.add_translate(pos_x, pos_y) if scale: grp.transform.add_scale(scale) # GENERATE THE QRCODE if opt.typenumber == 0: # Automatic QR code size` code = QRCode.getMinimumQRCode(text_data, opt.correctionlevel) else: # Manual QR code size code = QRCode(correction=opt.correctionlevel) code.setTypeNumber(int(opt.typenumber)) code.addData(text_data) code.make() self.boxsize = opt.modulesize self.invert_code = opt.invert self.margin = 4 self.draw = GridDrawer(opt.invert, opt.smoothval) self.draw.set_grid(code.modules) self.render_svg(grp, opt.drawtype) return grp def ring_iterator(self, vertices, greedy): visited = set() lut = defaultdict(list) for i, v in enumerate(vertices): lut[v[0:2]].append(i) def getnext(vertex): nextPos = self.draw.moveByDirection(vertex) nextIndexes = lut[nextPos[0:2]] if len(nextIndexes) == 0 or len(nextIndexes) > 2: raise Exception("Vertex " + str(next_c) + " has no connections") elif len(nextIndexes) == 1: vertsIndexNext = nextIndexes[0] else: if {vertices[nextIndexes[0]][2], vertices[nextIndexes[1]][2]} != { (vertex[2] - 1) % 4, (vertex[2] + 1) % 4, }: raise Exception( "Bad next vertex directions " + str(vertices[nextIndexes[0]]) + str(vertices[nextIndexes[1]]) ) # Greedy - CCW turn, proud and neutral CW turn vertsIndexNext = ( nextIndexes[0] if (greedy == "g") == (vertices[nextIndexes[0]][2] == (vertex[2] + 1) % 4) else nextIndexes[1] ) return vertices[vertsIndexNext] for start_vertex in reversed(vertices): if start_vertex in visited: continue path = [start_vertex] next_vertex = getnext(start_vertex) while start_vertex != next_vertex: path.append(next_vertex) next_vertex = getnext(next_vertex) else: yield path visited |= set(path) def render_adv(self, greedy): verts = self.draw.createVertexesForAdvDrawer() it = self.ring_iterator(verts, greedy) qrPath = inkex.Path() for ringVertices in it: posStart, _ = self.draw.getSmoothPosition(ringVertices[0]) qrPath.append(Move(*self.get_svg_pos(posStart[0], posStart[1]))) for vc, vn in circular_pairwise(ringVertices): if vn[2] != vc[2]: if (greedy != "n") or not vn[3]: # Add bezier # Opt length http://spencermortensen.com/articles/bezier-circle/ # c = 0.552284749 ex = 1 - 0.552284749 _, bs = self.draw.getSmoothPosition(vc) _, bp1 = self.draw.getSmoothPosition(vc, ex) bp2, _ = self.draw.getSmoothPosition(vn, ex) bf, _ = self.draw.getSmoothPosition(vn) qrPath.append(Line(*self.get_svg_pos(bs[0], bs[1]))) qrPath.append( Curve( *self.get_svg_pos(bp1[0], bp1[1]), *self.get_svg_pos(bp2[0], bp2[1]), *self.get_svg_pos(bf[0], bf[1]), ) ) else: # Add straight qrPath.append(Line(*self.get_svg_pos(vn[0], vn[1]))) qrPath.append(zoneClose()) return PathElement.new(path=qrPath) def render_obsolete(self): for row in range(self.draw.row_count): for col in range(self.draw.col_count): if self.draw.isDark(col, row): x, y = self.get_svg_pos(col, row) return Rectangle.new(x, y, self.boxsize, self.boxsize) def render_path(self, pointStr): singlePath = inkex.Path(self.get_icon_path_str(pointStr)) path = inkex.Path() for row in range(self.draw.row_count): for col in range(self.draw.col_count): if self.draw.isDark(col, row): x, y = self.get_svg_pos(col, row) path.append(Move(x, y)) path += singlePath path.append(zoneClose()) return PathElement.new(path=path) def render_selection(self): if len(self.svg.selection) > 0: self.options.symbolid = self.svg.selection.first().get_id() else: raise inkex.AbortExtension(_("Please select an element to clone")) return self.render_symbol() def render_symbol(self): symbol = self.svg.getElementById(self.options.symbolid) if symbol is None: raise inkex.AbortExtension( _("Can't find symbol {}").format(self.options.symbolid) ) bbox = symbol.path.bounding_box() transform = inkex.Transform( scale=( float(self.boxsize) / bbox.width, float(self.boxsize) / bbox.height, ) ) result = Group() for row in range(self.draw.row_count): for col in range(self.draw.col_count): if self.draw.isDark(col, row): x, y = self.get_svg_pos(col, row) # Inkscape doesn't support width/height on use tags result.append( Use.new( symbol, x / transform.a, y / transform.d, transform=transform, ) ) return result def render_pathpreset(self): if self.options.pathtype == "simple": return self.render_path("h 1 v 1 h -1") else: s = ( "m 0.5,0.5 " "c 0.2761423745,0 0.5,0.2238576255 0.5,0.5 " "c 0,0.2761423745 -0.2238576255,0.5 -0.5,0.5 " "c -0.2761423745,0 -0.5,-0.2238576255 -0.5,-0.5 " "c 0,-0.2761423745 0.2238576255,-0.5 0.5,-0.5" ) return self.render_path(s) render_smooth = lambda self: self.render_adv(self.options.smoothness[0]) def render_svg(self, grp, drawtype): """Render to svg""" drawer = getattr(self, f"render_{drawtype}", self.render_obsolete) if drawer is None: raise Exception("Unknown draw type: " + drawtype) canvas_width = (self.draw.col_count + 2 * self.margin) * self.boxsize canvas_height = (self.draw.row_count + 2 * self.margin) * self.boxsize # white background providing margin: rect = grp.add(Rectangle.new(0, 0, canvas_width, canvas_height)) rect.style["stroke"] = "none" rect.style["fill"] = "black" if self.invert_code else "white" qrg = grp.add(Group()) qrg.style["stroke"] = "none" qrg.style["fill"] = "white" if self.invert_code else "black" qrg.add(drawer()) def get_svg_pos(self, col, row): return (col + self.margin) * self.boxsize, (row + self.margin) * self.boxsize def get_icon_path_str(self, pointStr): result = "" digBuffer = "" for c in pointStr: if c.isdigit() or c == "-" or c == ".": digBuffer += c else: if len(digBuffer) > 0: result += str(float(digBuffer) * self.boxsize) digBuffer = "" result += c if len(digBuffer) > 0: result += str(float(digBuffer) * self.boxsize) return result if __name__ == "__main__": QrCode().run()