From bba83cad701c374d1c6b02c2263c4787f0467c31 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 10:21:54 +0900 Subject: [PATCH 001/113] fix: Raise too long error (#4) --- src/rmqrcode/console.py | 15 +++++++++------ src/rmqrcode/rmqrcode.py | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index 8e0324d..8d53340 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -53,12 +53,15 @@ def main(): elif args.fit_strategy == 'min_height': fit_strategy = FitStrategy.MINIMIZE_HEIGHT - qr = _make_qr( - args.DATA, - ecc=ecc, - version=args.version, - fit_strategy=fit_strategy - ) + try: + qr = _make_qr( + args.DATA, + ecc=ecc, + version=args.version, + fit_strategy=fit_strategy + ) + except DataTooLongError: + _show_error_and_exit("Error: The data is too long.") _save_image(qr, args.OUTPUT) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 87ab1c2..aadb4cd 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -265,6 +265,9 @@ def _put_data(self, data): encoded_data = self._convert_to_bites_data(data, character_count_length, codewords_total) codewords = split_into_8bits(encoded_data) + if len(codewords) > codewords_total: + raise DataTooLongError("The data is too long.") + # codeword数に満たない場合は規定の文字列を付与する while True: if len(codewords) >= codewords_total: From 17f78c6479d3058f0832d746eb19adad1a07e136 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 10:24:32 +0900 Subject: [PATCH 002/113] chore: Remove unnecessary print --- src/rmqrcode/console.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index 8e0324d..5fcd163 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -62,8 +62,6 @@ def main(): _save_image(qr, args.OUTPUT) - print(f"{qr}") - def _init_argparser(): parser = argparse.ArgumentParser() From 65d535e0b68c039765fc3fb9896850c19c372d89 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 10:41:07 +0900 Subject: [PATCH 003/113] doc: Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c4e63f..3e0ebdd 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ This is an rMQR Code image generator implemented in Python. This is implemented ## 📌 Important Notice -Please verify an image generated by this software whether it can decode correctly before use. - +- Please verify an image generated by this software whether it can decode correctly before use. +- Because this is in early stage, QR Code readers have not benn supported rMQR Code yet. ## 🚀 Installation ``` From cc3839d487add61439cbc15650acee590acc1ec0 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 11:14:30 +0900 Subject: [PATCH 004/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e0ebdd..6c054d1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This is an rMQR Code image generator implemented in Python. This is implemented ## 📌 Important Notice - Please verify an image generated by this software whether it can decode correctly before use. -- Because this is in early stage, QR Code readers have not benn supported rMQR Code yet. +- Because this is in early stage, QR Code readers may have not been supported rMQR Code yet. ## 🚀 Installation ``` From d4f1d2502f71e5ad3f8c32aa059be3c1082daaf5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 20:08:22 +0900 Subject: [PATCH 005/113] refactor: Use camel case --- src/rmqrcode/format/data_capacities.py | 2 +- .../format/{qr_versions.py => rmqr_versions.py} | 2 +- src/rmqrcode/rmqrcode.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/rmqrcode/format/{qr_versions.py => rmqr_versions.py} (99%) diff --git a/src/rmqrcode/format/data_capacities.py b/src/rmqrcode/format/data_capacities.py index 7cb8541..58b3739 100644 --- a/src/rmqrcode/format/data_capacities.py +++ b/src/rmqrcode/format/data_capacities.py @@ -2,7 +2,7 @@ # ISO/IEC 23941:2022 Table 6 -data_capacities = { +DataCapacities = { 'R7x43': { 'height': 7, 'width': 43, diff --git a/src/rmqrcode/format/qr_versions.py b/src/rmqrcode/format/rmqr_versions.py similarity index 99% rename from src/rmqrcode/format/qr_versions.py rename to src/rmqrcode/format/rmqr_versions.py index 1c29e8a..2bc9232 100644 --- a/src/rmqrcode/format/qr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -1,7 +1,7 @@ from .error_correction_level import ErrorCorrectionLevel -qr_versions = { +rMQRVersions = { 'R7x43': { 'version_indicator': 0b00000, 'height': 7, diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index aadb4cd..c8a2cad 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -1,6 +1,6 @@ from .format.error_correction_level import ErrorCorrectionLevel -from .format.qr_versions import qr_versions -from .format.data_capacities import data_capacities +from .format.rmqr_versions import rMQRVersions +from .format.data_capacities import DataCapacities from .format.alignment_pattern_coordinates import AlignmentPatternCoordinates from .format.generator_polynomials import GeneratorPolynomials from .format.mask import mask @@ -33,7 +33,7 @@ def fit(data,ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): determined_height = set() logger.debug("Select rMQR Code version") - for version_name, qr_version in data_capacities.items(): + for version_name, qr_version in DataCapacities.items(): if data_length <= qr_version['capacity']['Byte'][ecc]: width, height = qr_version['width'], qr_version['height'] if not width in determined_width and not height in determined_height: @@ -69,7 +69,7 @@ def __init__(self, version, ecc, logger=None): if not rMQR.validate_version(version): raise IllegalVersionError("The rMQR version is illegal.") - qr_version = qr_versions[version] + qr_version = rMQRVersions[version] self._version = version self._height = qr_version['height'] self._width = qr_version['width'] @@ -248,7 +248,7 @@ def _put_version_information_finder_sub_pattern_side(self, version_information): def _compute_version_info(self): - qr_version = qr_versions[self.version_name()] + qr_version = rMQRVersions[self.version_name()] version_information_data = qr_version['version_indicator'] if self._error_correction_level == ErrorCorrectionLevel.H: version_information_data |= 1<<6 @@ -258,7 +258,7 @@ def _compute_version_info(self): def _put_data(self, data): - qr_version = qr_versions[self.version_name()] + qr_version = rMQRVersions[self.version_name()] character_count_length = qr_version['character_count_length'] codewords_total = qr_version['codewords_total'] @@ -393,7 +393,7 @@ def _apply_mask(self, mask_area): @staticmethod def validate_version(version_name): - return version_name in qr_versions + return version_name in rMQRVersions class DataTooLongError(ValueError): From 9867e987a9644cd0e72be07d530f823d03d965e9 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 13 Jun 2022 20:35:43 +0900 Subject: [PATCH 006/113] chore: Translate the comments into English --- src/rmqrcode/rmqrcode.py | 40 ++++++++++++++---------------- src/rmqrcode/util/galois_fields.py | 2 +- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index c8a2cad..466934b 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -134,7 +134,7 @@ def __str__(self): def _put_finder_pattern(self): # Finder pattern - # 周囲 + # Outer square for i in range(7): for j in range(7): if i == 0 or i == 6 or j == 0 or j == 6: @@ -142,7 +142,7 @@ def _put_finder_pattern(self): else: self._qr[i][j] = Color.WHITE - # 真ん中 + # Inner square for i in range(3): for j in range(3): self._qr[2+i][2+j] = Color.BLACK @@ -156,19 +156,19 @@ def _put_finder_pattern(self): self._qr[7][n] = Color.WHITE # Finder sub pattern - # 周囲 + # Outer square for i in range(5): for j in range(5): color = Color.BLACK if i == 0 or i == 4 or j == 0 or j == 4 else Color.WHITE self._qr[self._height-i-1][self._width-j-1] = color - # 真ん中 + # Inner square self._qr[self._height-1-2][self._width-1-2] = Color.BLACK def _put_corner_finder_pattern(self): # Corner finder pattern - # 左下 + # Bottom left self._qr[self._height-1][0] = Color.BLACK self._qr[self._height-1][1] = Color.BLACK self._qr[self._height-1][2] = Color.BLACK @@ -177,7 +177,7 @@ def _put_corner_finder_pattern(self): self._qr[self._height-2][0] = Color.BLACK self._qr[self._height-2][1] = Color.WHITE - # 右上 + # Top right self._qr[0][self._width-1] = Color.BLACK self._qr[0][self._width-2] = Color.BLACK self._qr[1][self._width-1] = Color.BLACK @@ -191,22 +191,22 @@ def _put_alignment_pattern(self): for i in range(3): for j in range(3): color = Color.BLACK if i == 0 or i == 2 or j == 0 or j == 2 else Color.WHITE - # 上側 + # Top side self._qr[i][center_x + j - 1] = color - # 下側 + # Bottom side self._qr[self._height-1-i][center_x + j - 1] = color def _put_timing_pattern(self): # Timing pattern - # 横 + # Horizontal for j in range(self._width): color = Color.BLACK if (j + 1) % 2 else Color.WHITE for i in [0, self._height - 1]: if self._qr[i][j] == Color.UNDEFINED: self._qr[i][j] = color - # 縦 + # Vertical center_xs = [0, self._width - 1] center_xs.extend(AlignmentPatternCoordinates[self._width]) for i in range(self._height): @@ -268,7 +268,7 @@ def _put_data(self, data): if len(codewords) > codewords_total: raise DataTooLongError("The data is too long.") - # codeword数に満たない場合は規定の文字列を付与する + # Add the remainder codewords while True: if len(codewords) >= codewords_total: break @@ -282,7 +282,7 @@ def _put_data(self, data): qr_version['blocks'][self._error_correction_level] ) - # データの並び替え + # Construct the final message codeword sequence # Data codewords final_codewords = [] for i in range(len(data_codewords_per_block[-1])): @@ -300,8 +300,8 @@ def _put_data(self, data): final_codewords.append(rs_codewords[i]) self._logger.debug(f"Put RS data codewords {i} : {rs_codewords[i]}") - # 配置 - dy = -1 # 最初は上方向 + # Codeword placement + dy = -1 # Up current_codeword_idx = 0 current_bit_idx = 0 cx, cy = self._width - 2, self._height - 6 @@ -311,14 +311,14 @@ def _put_data(self, data): while True: for x in [cx, cx-1]: if self._qr[cy][x] == Color.UNDEFINED: - # 空白のセルのみ処理する + # Process only empty cell if current_codeword_idx == len(final_codewords): - # codewordsを配置しきった場合はremainder_bitsがあれば配置する + # Remainder bits self._qr[cy][x] = Color.WHITE mask_area[cy][x] = True remainder_bits -= 1 else: - # codewordsを配置する + # Codewords self._qr[cy][x] = Color.BLACK if final_codewords[current_codeword_idx][current_bit_idx] == '1' else Color.WHITE mask_area[cy][x] = True current_bit_idx += 1 @@ -326,15 +326,13 @@ def _put_data(self, data): current_bit_idx = 0 current_codeword_idx += 1 - # codewordsの配置が終わりremainder_bitsも残っていなければ終了 if current_codeword_idx == len(final_codewords) and remainder_bits == 0: break - # codewordsの配置が終わりremainder_bitsも残っていなければ終了 if current_codeword_idx == len(final_codewords) and remainder_bits == 0: break - # 座標の更新 + # Update current coordinates if dy < 0 and cy == 1: cx -= 2 dy = 1 @@ -372,7 +370,7 @@ def _split_into_blocks(self, codewords, blocks_definition): def _convert_to_bites_data(self, data, character_count_length, codewords_total): encoded_data = ByteEncoder.encode(data, character_count_length) - # 付加できるなら終端文字を付け加える + # Terminator (may be truncated) if len(encoded_data) + 3 <= codewords_total * 8: encoded_data += "000" diff --git a/src/rmqrcode/util/galois_fields.py b/src/rmqrcode/util/galois_fields.py index c23e210..6b3c076 100644 --- a/src/rmqrcode/util/galois_fields.py +++ b/src/rmqrcode/util/galois_fields.py @@ -4,7 +4,7 @@ class GaloisFields: i2e = {} def __init__(self): - # GF(2^8)の既約多項式 + # Irreducible polynomial in GF(2^8) p = (1<<8)|(1<<4)|(1<<3)|(1<<2)|1 self.e2i[0] = 1 From 3a2f815b02e6322ee4df3435d3d3c873ca8b6a8f Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 14 Jun 2022 22:30:41 +0900 Subject: [PATCH 007/113] chore: Version up to v0.1.3 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bdb177a..ada13c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = rmqrcode -version = 0.1.2 +version = 0.1.3 author = Takahiro Tomita author_email = ttp8101@gmail.com description = An rMQR Code Generetor From 0da56fd2460df121cdff69a75a322614e74485f0 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 24 Jun 2022 22:46:57 +0900 Subject: [PATCH 008/113] doc: Update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6c054d1..d8c9c94 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,14 @@ This is an rMQR Code image generator implemented in Python. This is implemented based on [ISO/IEC 23941:2022](https://www.iso.org/standard/77404.html). +Try this online (in Japanese): https://rmqr.oudon.xyz . + ## 📌 Important Notice - Please verify an image generated by this software whether it can decode correctly before use. - Because this is in early stage, QR Code readers may have not been supported rMQR Code yet. + ## 🚀 Installation ``` pip install rmqrcode From bf16068b25865d58666ac131436bdb45e6d7f606 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sat, 25 Jun 2022 13:51:53 +0900 Subject: [PATCH 009/113] doc: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8c9c94..9eb177e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The `fit_strategy` parameter is enum value of `rmqrcode.FitStrategy` to specify - **`FitStrategy.BALANCED`**: Try to keep balance of width and height. Here is an example of images genereated by each fit strategies for data `Test test test`: -![Example of fit strategies](https://user-images.githubusercontent.com/14174940/172822478-4f2b5fb8-49bd-464f-b6b2-c7347f71cbf5.png) +![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175758463-07a8e10d-faf8-4e56-8a01-356443e82d19.png) ### Save as image ```py From 34c42812f62b94b785f3a4b46193a444e3bd0be5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sat, 25 Jun 2022 14:01:45 +0900 Subject: [PATCH 010/113] doc: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9eb177e..4a05087 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The `fit_strategy` parameter is enum value of `rmqrcode.FitStrategy` to specify - **`FitStrategy.BALANCED`**: Try to keep balance of width and height. Here is an example of images genereated by each fit strategies for data `Test test test`: -![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175758463-07a8e10d-faf8-4e56-8a01-356443e82d19.png) +![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175758907-714ae349-c81d-4027-86fc-59360d557f18.png) ### Save as image ```py From bf994226dc5c20d6ea09a72017ca11d052cf08f8 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sat, 25 Jun 2022 14:08:25 +0900 Subject: [PATCH 011/113] doc: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a05087..d12de5e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The `fit_strategy` parameter is enum value of `rmqrcode.FitStrategy` to specify - **`FitStrategy.BALANCED`**: Try to keep balance of width and height. Here is an example of images genereated by each fit strategies for data `Test test test`: -![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175758907-714ae349-c81d-4027-86fc-59360d557f18.png) +![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175759120-7fb5ec71-c258-4646-9b91-6865b3eeac3f.png) ### Save as image ```py From 636dea6e3ef9e4c8001ab869b6d7c7305ad509d5 Mon Sep 17 00:00:00 2001 From: Ar-Ray-code Date: Sun, 24 Jul 2022 16:13:20 +0900 Subject: [PATCH 012/113] add `get_ndarray` --- example.py | 9 +++++++++ src/rmqrcode/qr_image.py | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 7e11b6c..1da5495 100644 --- a/example.py +++ b/example.py @@ -5,6 +5,7 @@ import logging +USE_NUMPY = True def main(): data = "https://oudon.xyz" @@ -26,6 +27,14 @@ def main(): image.show() image.save("my_qr.png") + # Convert to numpy array + if USE_NUMPY: + import cv2 + img = image.get_ndarray() + cv2.imshow("img", img) + cv2.waitKey(0) + cv2.destroyAllWindows() + def _init_logger(): logger = logging.getLogger() diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index a94050f..31b1d07 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -3,7 +3,6 @@ from PIL import Image from PIL import ImageDraw - class QRImage: def __init__(self, qr, module_size=10): self._module_size = module_size @@ -14,11 +13,17 @@ def __init__(self, qr, module_size=10): ) self._make_image(qr) - def show(self): self._img.show() pass + def get_ndarray(self): + try: + import numpy as np + except ImportError: + raise ImportError("numpy is not installed") + + return np.array(self._img) def save(self, name): self._img.save(name) From b26089ee406211ed49c00c115e72ccd02baa0ff0 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 27 Jul 2022 09:30:31 +0900 Subject: [PATCH 013/113] doc: Create CONTRIBUTING.md --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..53b92d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing to rmqrcode-python + +Thank you for interesting in contributing to rmqrcode-python! Any suggestions are welcome. + +## Style Guides +### Git Commit Message + +Consider starting commit message with one of the following prefixes. +- `feat:` : New feature +- `fix:` : Bug fix +- `refactor:` : Refactoring +- `chore:` : Little things +- `doc:` : Documentation From a3609859748708d36459abb14e465040f9bb1511 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 27 Jul 2022 09:57:52 +0900 Subject: [PATCH 014/113] doc: Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d12de5e..43f8c4d 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ image.save("my_qr.png") ``` -## 📚 Advanced Usage +## 📙 Advanced Usage ### Select rMQR Code size manually To select rMQR Code size manually, use `rMQR()` constructor. ```py @@ -111,6 +111,16 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Byte|✅| |Kanji|| + +## 🤝 Contiributing +Any suggestions are welcome! If you are interesting in contiributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). + + +## 📚 References +- [Rectangular Micro QR Code (rMQR) bar code symbology specification: ISO/IEC 23941:2022](https://www.iso.org/standard/77404.html) +- [Creating a QR Code step by step](https://www.nayuki.io/page/creating-a-qr-code-step-by-step) + + ---- The word "QR Code" is registered trademark of DENSO WAVE Incorporated.
http://www.denso-wave.com/qrcode/faqpatent-e.html From d7c70d9a8d5151430786c190209a39f20a695f9a Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 27 Jul 2022 21:02:19 +0900 Subject: [PATCH 015/113] feat: Use numpy only when numpy and cv2 are installed --- example.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 1da5495..16f7eb2 100644 --- a/example.py +++ b/example.py @@ -5,7 +5,14 @@ import logging -USE_NUMPY = True + +try: + import numpy + import cv2 + USE_NUMPY = True +except ImportError: + USE_NUMPY = False + def main(): data = "https://oudon.xyz" @@ -29,7 +36,6 @@ def main(): # Convert to numpy array if USE_NUMPY: - import cv2 img = image.get_ndarray() cv2.imshow("img", img) cv2.waitKey(0) From 37c33623d5897c58015fedaf9ec21570b88289e5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:06:22 +0900 Subject: [PATCH 016/113] doc: Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53b92d3..a6f9a73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,4 +10,5 @@ Consider starting commit message with one of the following prefixes. - `fix:` : Bug fix - `refactor:` : Refactoring - `chore:` : Little things +- `ci`: CI - `doc:` : Documentation From a4734c20f4eef4e98c96be3560ccfe177a4188ea Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:19:12 +0900 Subject: [PATCH 017/113] ci: Setup linter --- Makefile | 10 ++++++++++ pyproject.toml | 5 ++++- setup.cfg | 9 ++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..795901d --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: lint +lint: + flake8 src + isort --check --diff src + black --check src + +.PHONY: format +format: + isort src + black src \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fa7093a..29402e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ [build-system] requires = ["setuptools>=42"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 119 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index ada13c1..0ffa01e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,10 +23,17 @@ install_requires = [options.extras_require] dev = pytest + flake8 + isort + black [options.packages.find] where = src [options.entry_points] console_scripts = - rmqr = rmqrcode.console:main \ No newline at end of file + rmqr = rmqrcode.console:main + +[flake8] +max-line-length = 119 +extend-ignore = E203 \ No newline at end of file From 30541e1f6fd1ad9edcc7d6cec1d5a1c4e2daf8dd Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:27:35 +0900 Subject: [PATCH 018/113] ci: Exclude generator_polynomials.py from the lint targets --- pyproject.toml | 3 ++- setup.cfg | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 29402e7..9e5326f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,5 @@ requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" [tool.black] -line-length = 119 \ No newline at end of file +line-length = 119 +exclude = "generator_polynomials.py" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 0ffa01e..99662bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,4 +36,5 @@ console_scripts = [flake8] max-line-length = 119 -extend-ignore = E203 \ No newline at end of file +extend-ignore = E203 +exclude = src/rmqrcode/format/generator_polynomials.py \ No newline at end of file From a5ffd9ddefca679d7c1208f679e3065a0d63e003 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:32:08 +0900 Subject: [PATCH 019/113] refactor: Run formatter --- src/rmqrcode/__init__.py | 7 +- src/rmqrcode/console.py | 46 +- src/rmqrcode/encoder/byte_encoder.py | 6 +- src/rmqrcode/enums/color.py | 3 +- src/rmqrcode/enums/fit_strategy.py | 3 +- .../format/alignment_pattern_coordinates.py | 2 +- src/rmqrcode/format/data_capacities.py | 323 +++-- src/rmqrcode/format/error_correction_level.py | 3 +- src/rmqrcode/format/mask.py | 2 +- src/rmqrcode/format/rmqr_versions.py | 1143 ++++++++--------- src/rmqrcode/qr_image.py | 26 +- src/rmqrcode/rmqrcode.py | 146 +-- src/rmqrcode/util/error_correction.py | 16 +- src/rmqrcode/util/galois_fields.py | 4 +- src/rmqrcode/util/utilities.py | 6 +- 15 files changed, 863 insertions(+), 873 deletions(-) diff --git a/src/rmqrcode/__init__.py b/src/rmqrcode/__init__.py index 2b40cee..c537672 100644 --- a/src/rmqrcode/__init__.py +++ b/src/rmqrcode/__init__.py @@ -1,6 +1,3 @@ -from .rmqrcode import rMQR -from .rmqrcode import FitStrategy -from .rmqrcode import DataTooLongError -from .rmqrcode import IllegalVersionError +from .format.error_correction_level import ErrorCorrectionLevel from .qr_image import QRImage -from .format.error_correction_level import ErrorCorrectionLevel \ No newline at end of file +from .rmqrcode import DataTooLongError, FitStrategy, IllegalVersionError, rMQR diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index aa1117e..615ce29 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -1,15 +1,10 @@ #!/usr/bin/env python -import rmqrcode -from rmqrcode import rMQR -from rmqrcode import QRImage -from rmqrcode import ErrorCorrectionLevel -from rmqrcode import FitStrategy -from rmqrcode import DataTooLongError -from rmqrcode import IllegalVersionError - import argparse import sys +import rmqrcode +from rmqrcode import DataTooLongError, ErrorCorrectionLevel, FitStrategy, IllegalVersionError, QRImage, rMQR + def _show_error_and_exit(msg): print(msg, file=sys.stderr) @@ -29,7 +24,6 @@ def _make_qr(data, ecc, version, fit_strategy): return qr - def _save_image(qr, output): image = QRImage(qr) try: @@ -42,24 +36,19 @@ def main(): parser = _init_argparser() args = parser.parse_args() - if args.ecc == 'M': + if args.ecc == "M": ecc = ErrorCorrectionLevel.M - elif args.ecc == 'H': + elif args.ecc == "H": ecc = ErrorCorrectionLevel.H fit_strategy = FitStrategy.BALANCED - if args.fit_strategy == 'min_width': + if args.fit_strategy == "min_width": fit_strategy = FitStrategy.MINIMIZE_WIDTH - elif args.fit_strategy == 'min_height': + elif args.fit_strategy == "min_height": fit_strategy = FitStrategy.MINIMIZE_HEIGHT try: - qr = _make_qr( - args.DATA, - ecc=ecc, - version=args.version, - fit_strategy=fit_strategy - ) + qr = _make_qr(args.DATA, ecc=ecc, version=args.version, fit_strategy=fit_strategy) except DataTooLongError: _show_error_and_exit("Error: The data is too long.") @@ -68,13 +57,20 @@ def main(): def _init_argparser(): parser = argparse.ArgumentParser() - parser.add_argument('DATA', type=str, help="Data to encode.") - parser.add_argument('OUTPUT', type=str, help="Output file path") - parser.add_argument('--ecc', help="Error correction level. (default: M)", type=str, choices=["M", "H"], default='M') - parser.add_argument('--version', help="rMQR Code version like 'R11x139'.") - parser.add_argument('--fit-strategy', choices=["min_width", "min_height", "balanced"], help="Strategy how to determine rMQR Code size.", dest="fit_strategy") + parser.add_argument("DATA", type=str, help="Data to encode.") + parser.add_argument("OUTPUT", type=str, help="Output file path") + parser.add_argument( + "--ecc", help="Error correction level. (default: M)", type=str, choices=["M", "H"], default="M" + ) + parser.add_argument("--version", help="rMQR Code version like 'R11x139'.") + parser.add_argument( + "--fit-strategy", + choices=["min_width", "min_height", "balanced"], + help="Strategy how to determine rMQR Code size.", + dest="fit_strategy", + ) return parser if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index 38b7c22..ed6bbdf 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -4,12 +4,11 @@ class ByteEncoder: @staticmethod def _encoded_bits(s): res = "" - encoded = s.encode('utf-8') + encoded = s.encode("utf-8") for byte in encoded: res += bin(byte)[2:].zfill(8) return res - @staticmethod def encode(data, character_count_length): res = ByteEncoder.MODE_INDICATOR @@ -17,7 +16,6 @@ def encode(data, character_count_length): res += ByteEncoder._encoded_bits(data) return res - @staticmethod def length(data): - return len(data.encode('utf-8')) + return len(data.encode("utf-8")) diff --git a/src/rmqrcode/enums/color.py b/src/rmqrcode/enums/color.py index 7aeb18d..324839b 100644 --- a/src/rmqrcode/enums/color.py +++ b/src/rmqrcode/enums/color.py @@ -1,6 +1,7 @@ from enum import Enum + class Color(Enum): UNDEFINED = -1 WHITE = 0 - BLACK = 1 \ No newline at end of file + BLACK = 1 diff --git a/src/rmqrcode/enums/fit_strategy.py b/src/rmqrcode/enums/fit_strategy.py index bd006e9..80588b3 100644 --- a/src/rmqrcode/enums/fit_strategy.py +++ b/src/rmqrcode/enums/fit_strategy.py @@ -1,6 +1,7 @@ from enum import Enum + class FitStrategy(Enum): MINIMIZE_WIDTH = 0 MINIMIZE_HEIGHT = 1 - BALANCED = 2 \ No newline at end of file + BALANCED = 2 diff --git a/src/rmqrcode/format/alignment_pattern_coordinates.py b/src/rmqrcode/format/alignment_pattern_coordinates.py index 249698b..7b95c3f 100644 --- a/src/rmqrcode/format/alignment_pattern_coordinates.py +++ b/src/rmqrcode/format/alignment_pattern_coordinates.py @@ -5,4 +5,4 @@ 77: [25, 51], 99: [23, 49, 75], 139: [27, 55, 83, 111], -} \ No newline at end of file +} diff --git a/src/rmqrcode/format/data_capacities.py b/src/rmqrcode/format/data_capacities.py index 58b3739..304f067 100644 --- a/src/rmqrcode/format/data_capacities.py +++ b/src/rmqrcode/format/data_capacities.py @@ -1,326 +1,325 @@ from .error_correction_level import ErrorCorrectionLevel - # ISO/IEC 23941:2022 Table 6 DataCapacities = { - 'R7x43': { - 'height': 7, - 'width': 43, - 'capacity': { - 'Byte': { + "R7x43": { + "height": 7, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 5, ErrorCorrectionLevel.H: 2, }, }, }, - 'R7x59': { - 'height': 7, - 'width': 59, - 'capacity': { - 'Byte': { + "R7x59": { + "height": 7, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R7x77': { - 'height': 7, - 'width': 77, - 'capacity': { - 'Byte': { + "R7x77": { + "height": 7, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 19, ErrorCorrectionLevel.H: 9, }, }, }, - 'R7x99': { - 'height': 7, - 'width': 99, - 'capacity': { - 'Byte': { + "R7x99": { + "height": 7, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 27, ErrorCorrectionLevel.H: 13, }, }, }, - 'R7x139': { - 'height': 7, - 'width': 139, - 'capacity': { - 'Byte': { + "R7x139": { + "height": 7, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 42, ErrorCorrectionLevel.H: 22, }, }, }, - 'R9x43': { - 'height': 9, - 'width': 43, - 'capacity': { - 'Byte': { + "R9x43": { + "height": 9, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R9x59': { - 'height': 9, - 'width': 59, - 'capacity': { - 'Byte': { + "R9x59": { + "height": 9, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 20, ErrorCorrectionLevel.H: 10, }, }, }, - 'R9x77': { - 'height': 9, - 'width': 77, - 'capacity': { - 'Byte': { + "R9x77": { + "height": 9, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 30, ErrorCorrectionLevel.H: 16, }, }, }, - 'R9x99': { - 'height': 9, - 'width': 99, - 'capacity': { - 'Byte': { + "R9x99": { + "height": 9, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 40, ErrorCorrectionLevel.H: 20, }, }, }, - 'R9x139': { - 'height': 9, - 'width': 139, - 'capacity': { - 'Byte': { + "R9x139": { + "height": 9, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 61, ErrorCorrectionLevel.H: 31, }, }, }, - 'R11x27': { - 'height': 11, - 'width': 27, - 'capacity': { - 'Byte': { + "R11x27": { + "height": 11, + "width": 27, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 6, ErrorCorrectionLevel.H: 4, }, }, }, - 'R11x43': { - 'height': 11, - 'width': 43, - 'capacity': { - 'Byte': { + "R11x43": { + "height": 11, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 18, ErrorCorrectionLevel.H: 10, }, }, }, - 'R11x59': { - 'height': 11, - 'width': 59, - 'capacity': { - 'Byte': { + "R11x59": { + "height": 11, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 30, ErrorCorrectionLevel.H: 14, }, }, }, - 'R11x77': { - 'height': 11, - 'width': 77, - 'capacity': { - 'Byte': { + "R11x77": { + "height": 11, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 41, ErrorCorrectionLevel.H: 21, }, }, }, - 'R11x99': { - 'height': 11, - 'width': 99, - 'capacity': { - 'Byte': { + "R11x99": { + "height": 11, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 55, ErrorCorrectionLevel.H: 27, }, }, }, - 'R11x139': { - 'height': 11, - 'width': 139, - 'capacity': { - 'Byte': { + "R11x139": { + "height": 11, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 82, ErrorCorrectionLevel.H: 40, }, }, }, - 'R13x27': { - 'height': 13, - 'width': 27, - 'capacity': { - 'Byte': { + "R13x27": { + "height": 13, + "width": 27, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R13x43': { - 'height': 13, - 'width': 43, - 'capacity': { - 'Byte': { + "R13x43": { + "height": 13, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 26, ErrorCorrectionLevel.H: 12, }, }, }, - 'R13x59': { - 'height': 13, - 'width': 59, - 'capacity': { - 'Byte': { + "R13x59": { + "height": 13, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 36, ErrorCorrectionLevel.H: 18, }, }, }, - 'R13x77': { - 'height': 13, - 'width': 77, - 'capacity': { - 'Byte': { + "R13x77": { + "height": 13, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 51, ErrorCorrectionLevel.H: 27, }, }, }, - 'R13x99': { - 'height': 13, - 'width': 99, - 'capacity': { - 'Byte': { + "R13x99": { + "height": 13, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 71, ErrorCorrectionLevel.H: 33, }, }, }, - 'R13x139': { - 'height': 13, - 'width': 139, - 'capacity': { - 'Byte': { + "R13x139": { + "height": 13, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 104, ErrorCorrectionLevel.H: 52, }, }, }, - 'R15x43': { - 'height': 15, - 'width': 43, - 'capacity': { - 'Byte': { + "R15x43": { + "height": 15, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 31, ErrorCorrectionLevel.H: 13, }, }, }, - 'R15x59': { - 'height': 15, - 'width': 59, - 'capacity': { - 'Byte': { + "R15x59": { + "height": 15, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 46, ErrorCorrectionLevel.H: 24, }, }, }, - 'R15x77': { - 'height': 15, - 'width': 77, - 'capacity': { - 'Byte': { + "R15x77": { + "height": 15, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 65, ErrorCorrectionLevel.H: 29, }, }, }, - 'R15x99': { - 'height': 15, - 'width': 99, - 'capacity': { - 'Byte': { + "R15x99": { + "height": 15, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 86, ErrorCorrectionLevel.H: 46, }, }, }, - 'R15x139': { - 'height': 15, - 'width': 139, - 'capacity': { - 'Byte': { + "R15x139": { + "height": 15, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 125, ErrorCorrectionLevel.H: 67, }, }, }, - 'R17x43': { - 'height': 17, - 'width': 43, - 'capacity': { - 'Byte': { + "R17x43": { + "height": 17, + "width": 43, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 37, ErrorCorrectionLevel.H: 19, }, }, }, - 'R17x59': { - 'height': 17, - 'width': 59, - 'capacity': { - 'Byte': { + "R17x59": { + "height": 17, + "width": 59, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 54, ErrorCorrectionLevel.H: 26, }, }, }, - 'R17x77': { - 'height': 17, - 'width': 77, - 'capacity': { - 'Byte': { + "R17x77": { + "height": 17, + "width": 77, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 76, ErrorCorrectionLevel.H: 36, }, }, }, - 'R17x99': { - 'height': 17, - 'width': 99, - 'capacity': { - 'Byte': { + "R17x99": { + "height": 17, + "width": 99, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 98, ErrorCorrectionLevel.H: 54, }, }, }, - 'R17x139': { - 'height': 17, - 'width': 139, - 'capacity': { - 'Byte': { + "R17x139": { + "height": 17, + "width": 139, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 150, ErrorCorrectionLevel.H: 74, }, }, }, -} \ No newline at end of file +} diff --git a/src/rmqrcode/format/error_correction_level.py b/src/rmqrcode/format/error_correction_level.py index 7483698..84e18e3 100644 --- a/src/rmqrcode/format/error_correction_level.py +++ b/src/rmqrcode/format/error_correction_level.py @@ -1,5 +1,6 @@ from enum import Enum + class ErrorCorrectionLevel(Enum): M = 0 - H = 1 \ No newline at end of file + H = 1 diff --git a/src/rmqrcode/format/mask.py b/src/rmqrcode/format/mask.py index 4822236..4ce92b7 100644 --- a/src/rmqrcode/format/mask.py +++ b/src/rmqrcode/format/mask.py @@ -1,2 +1,2 @@ def mask(x, y): - return (y//2 + x//3) % 2 == 0 + return (y // 2 + x // 3) % 2 == 0 diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index 2bc9232..6861772 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -1,878 +1,877 @@ from .error_correction_level import ErrorCorrectionLevel - rMQRVersions = { - 'R7x43': { - 'version_indicator': 0b00000, - 'height': 7, - 'width': 43, - 'remainder_bits': 0, - 'character_count_length': 3, - 'codewords_total': 13, - 'blocks': { + "R7x43": { + "version_indicator": 0b00000, + "height": 7, + "width": 43, + "remainder_bits": 0, + "character_count_length": 3, + "codewords_total": 13, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 13, - 'k': 6, + "num": 1, + "c": 13, + "k": 6, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 13, - 'k': 3, + "num": 1, + "c": 13, + "k": 3, }, - ] - } + ], + }, }, - 'R7x59': { - 'version_indicator': 0b00001, - 'height': 7, - 'width': 59, - 'remainder_bits': 3, - 'character_count_length': 4, - 'codewords_total': 21, - 'blocks': { + "R7x59": { + "version_indicator": 0b00001, + "height": 7, + "width": 59, + "remainder_bits": 3, + "character_count_length": 4, + "codewords_total": 21, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 21, - 'k': 12, + "num": 1, + "c": 21, + "k": 12, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 21, - 'k': 7, + "num": 1, + "c": 21, + "k": 7, }, - ] - } + ], + }, }, - 'R7x77': { - 'version_indicator': 0b00010, - 'height': 7, - 'width': 77, - 'remainder_bits': 5, - 'character_count_length': 5, - 'codewords_total': 32, - 'blocks': { + "R7x77": { + "version_indicator": 0b00010, + "height": 7, + "width": 77, + "remainder_bits": 5, + "character_count_length": 5, + "codewords_total": 32, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 32, - 'k': 20, + "num": 1, + "c": 32, + "k": 20, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 32, - 'k': 10, + "num": 1, + "c": 32, + "k": 10, }, - ] - } + ], + }, }, - 'R7x99': { - 'version_indicator': 0b00011, - 'height': 7, - 'width': 99, - 'remainder_bits': 6, - 'character_count_length': 5, - 'codewords_total': 44, - 'blocks': { + "R7x99": { + "version_indicator": 0b00011, + "height": 7, + "width": 99, + "remainder_bits": 6, + "character_count_length": 5, + "codewords_total": 44, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 44, - 'k': 28, + "num": 1, + "c": 44, + "k": 28, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 44, - 'k': 14, + "num": 1, + "c": 44, + "k": 14, }, - ] - } + ], + }, }, - 'R7x139': { - 'version_indicator': 0b00100, - 'height': 7, - 'width': 139, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 68, - 'blocks': { + "R7x139": { + "version_indicator": 0b00100, + "height": 7, + "width": 139, + "remainder_bits": 1, + "character_count_length": 6, + "codewords_total": 68, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 68, - 'k': 44, + "num": 1, + "c": 68, + "k": 44, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 34, - 'k': 12, + "num": 2, + "c": 34, + "k": 12, }, - ] - } + ], + }, }, - 'R9x43': { - 'version_indicator': 0b00101, - 'height': 9, - 'width': 43, - 'remainder_bits': 2, - 'character_count_length': 4, - 'codewords_total': 21, - 'blocks': { + "R9x43": { + "version_indicator": 0b00101, + "height": 9, + "width": 43, + "remainder_bits": 2, + "character_count_length": 4, + "codewords_total": 21, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 21, - 'k': 12, + "num": 1, + "c": 21, + "k": 12, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 21, - 'k': 7, + "num": 1, + "c": 21, + "k": 7, }, - ] - } + ], + }, }, - 'R9x59': { - 'version_indicator': 0b00110, - 'height': 9, - 'width': 59, - 'remainder_bits': 3, - 'character_count_length': 5, - 'codewords_total': 33, - 'blocks': { + "R9x59": { + "version_indicator": 0b00110, + "height": 9, + "width": 59, + "remainder_bits": 3, + "character_count_length": 5, + "codewords_total": 33, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 33, - 'k': 21, + "num": 1, + "c": 33, + "k": 21, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 33, - 'k': 11, + "num": 1, + "c": 33, + "k": 11, }, - ] - } + ], + }, }, - 'R9x77': { - 'version_indicator': 0b00111, - 'height': 9, - 'width': 77, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 49, - 'blocks': { + "R9x77": { + "version_indicator": 0b00111, + "height": 9, + "width": 77, + "remainder_bits": 1, + "character_count_length": 5, + "codewords_total": 49, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 49, - 'k': 31, + "num": 1, + "c": 49, + "k": 31, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 24, - 'k': 8, + "num": 1, + "c": 24, + "k": 8, }, { - 'num': 1, - 'c': 25, - 'k': 9, + "num": 1, + "c": 25, + "k": 9, }, - ] - } + ], + }, }, - 'R9x99': { - 'version_indicator': 0b01000, - 'height': 9, - 'width': 99, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 66, - 'blocks': { + "R9x99": { + "version_indicator": 0b01000, + "height": 9, + "width": 99, + "remainder_bits": 4, + "character_count_length": 6, + "codewords_total": 66, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 66, - 'k': 42, + "num": 1, + "c": 66, + "k": 42, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 33, - 'k': 11, + "num": 2, + "c": 33, + "k": 11, }, - ] - } + ], + }, }, - 'R9x139': { - 'version_indicator': 0b01001, - 'height': 9, - 'width': 139, - 'remainder_bits': 5, - 'character_count_length': 6, - 'codewords_total': 99, - 'blocks': { + "R9x139": { + "version_indicator": 0b01001, + "height": 9, + "width": 139, + "remainder_bits": 5, + "character_count_length": 6, + "codewords_total": 99, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 49, - 'k': 31, + "num": 1, + "c": 49, + "k": 31, }, { - 'num': 1, - 'c': 50, - 'k': 32, + "num": 1, + "c": 50, + "k": 32, }, ], ErrorCorrectionLevel.H: [ { - 'num': 3, - 'c': 33, - 'k': 11, + "num": 3, + "c": 33, + "k": 11, } - ] - } + ], + }, }, - 'R11x27': { - 'version_indicator': 0b01010, - 'height': 11, - 'width': 27, - 'remainder_bits': 2, - 'character_count_length': 3, - 'codewords_total': 15, - 'blocks': { + "R11x27": { + "version_indicator": 0b01010, + "height": 11, + "width": 27, + "remainder_bits": 2, + "character_count_length": 3, + "codewords_total": 15, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 15, - 'k': 7, + "num": 1, + "c": 15, + "k": 7, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 15, - 'k': 5, + "num": 1, + "c": 15, + "k": 5, } - ] - } + ], + }, }, - 'R11x43': { - 'version_indicator': 0b01011, - 'height': 11, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 31, - 'blocks': { + "R11x43": { + "version_indicator": 0b01011, + "height": 11, + "width": 43, + "remainder_bits": 1, + "character_count_length": 5, + "codewords_total": 31, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 31, - 'k': 19, + "num": 1, + "c": 31, + "k": 19, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 31, - 'k': 11, + "num": 1, + "c": 31, + "k": 11, } - ] - } + ], + }, }, - 'R11x59': { - 'version_indicator': 0b01100, - 'height': 11, - 'width': 59, - 'remainder_bits': 0, - 'character_count_length': 5, - 'codewords_total': 47, - 'blocks': { + "R11x59": { + "version_indicator": 0b01100, + "height": 11, + "width": 59, + "remainder_bits": 0, + "character_count_length": 5, + "codewords_total": 47, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 47, - 'k': 31, + "num": 1, + "c": 47, + "k": 31, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 23, - 'k': 7, + "num": 1, + "c": 23, + "k": 7, }, { - 'num': 1, - 'c': 24, - 'k': 8, + "num": 1, + "c": 24, + "k": 8, }, - ] - } + ], + }, }, - 'R11x77': { - 'version_indicator': 0b01101, - 'height': 11, - 'width': 77, - 'remainder_bits': 2, - 'character_count_length': 6, - 'codewords_total': 67, - 'blocks': { + "R11x77": { + "version_indicator": 0b01101, + "height": 11, + "width": 77, + "remainder_bits": 2, + "character_count_length": 6, + "codewords_total": 67, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 67, - 'k': 43, + "num": 1, + "c": 67, + "k": 43, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 33, - 'k': 11, + "num": 1, + "c": 33, + "k": 11, }, { - 'num': 1, - 'c': 34, - 'k': 12, + "num": 1, + "c": 34, + "k": 12, }, - ] - } + ], + }, }, - 'R11x99': { - 'version_indicator': 0b01110, - 'height': 11, - 'width': 99, - 'remainder_bits': 7, - 'character_count_length': 6, - 'codewords_total': 89, - 'blocks': { + "R11x99": { + "version_indicator": 0b01110, + "height": 11, + "width": 99, + "remainder_bits": 7, + "character_count_length": 6, + "codewords_total": 89, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 44, - 'k': 28, + "num": 1, + "c": 44, + "k": 28, }, { - 'num': 1, - 'c': 45, - 'k': 29, + "num": 1, + "c": 45, + "k": 29, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 44, - 'k': 14, + "num": 1, + "c": 44, + "k": 14, }, { - 'num': 1, - 'c': 45, - 'k': 15, + "num": 1, + "c": 45, + "k": 15, }, - ] - } + ], + }, }, - 'R11x139': { - 'version_indicator': 0b01111, - 'height': 11, - 'width': 139, - 'remainder_bits': 6, - 'character_count_length': 7, - 'codewords_total': 132, - 'blocks': { + "R11x139": { + "version_indicator": 0b01111, + "height": 11, + "width": 139, + "remainder_bits": 6, + "character_count_length": 7, + "codewords_total": 132, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 66, - 'k': 42, + "num": 2, + "c": 66, + "k": 42, }, ], ErrorCorrectionLevel.H: [ { - 'num': 3, - 'c': 44, - 'k': 14, + "num": 3, + "c": 44, + "k": 14, } - ] - } + ], + }, }, - 'R13x27': { - 'version_indicator': 0b10000, - 'height': 13, - 'width': 27, - 'character_count_length': 4, - 'remainder_bits': 4, - 'codewords_total': 21, - 'blocks': { + "R13x27": { + "version_indicator": 0b10000, + "height": 13, + "width": 27, + "character_count_length": 4, + "remainder_bits": 4, + "codewords_total": 21, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 21, - 'k': 14, + "num": 1, + "c": 21, + "k": 14, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 21, - 'k': 7, + "num": 1, + "c": 21, + "k": 7, } - ] - } + ], + }, }, - 'R13x43': { - 'version_indicator': 0b10001, - 'height': 13, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 41, - 'blocks': { + "R13x43": { + "version_indicator": 0b10001, + "height": 13, + "width": 43, + "remainder_bits": 1, + "character_count_length": 5, + "codewords_total": 41, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 41, - 'k': 27, + "num": 1, + "c": 41, + "k": 27, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 41, - 'k': 13, + "num": 1, + "c": 41, + "k": 13, } - ] - } + ], + }, }, - 'R13x59': { - 'version_indicator': 0b10010, - 'height': 13, - 'width': 59, - 'remainder_bits': 6, - 'character_count_length': 6, - 'codewords_total': 60, - 'blocks': { + "R13x59": { + "version_indicator": 0b10010, + "height": 13, + "width": 59, + "remainder_bits": 6, + "character_count_length": 6, + "codewords_total": 60, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 60, - 'k': 38, + "num": 1, + "c": 60, + "k": 38, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 30, - 'k': 10, + "num": 2, + "c": 30, + "k": 10, } - ] - } + ], + }, }, - 'R13x77': { - 'version_indicator': 0b10011, - 'height': 13, - 'width': 77, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 85, - 'blocks': { + "R13x77": { + "version_indicator": 0b10011, + "height": 13, + "width": 77, + "remainder_bits": 4, + "character_count_length": 6, + "codewords_total": 85, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 42, - 'k': 26, + "num": 1, + "c": 42, + "k": 26, }, { - 'num': 1, - 'c': 43, - 'k': 27, + "num": 1, + "c": 43, + "k": 27, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 42, - 'k': 14, + "num": 1, + "c": 42, + "k": 14, }, { - 'num': 1, - 'c': 43, - 'k': 15, + "num": 1, + "c": 43, + "k": 15, }, - ] - } + ], + }, }, - 'R13x99': { - 'version_indicator': 0b10100, - 'height': 13, - 'width': 99, - 'remainder_bits': 3, - 'character_count_length': 7, - 'codewords_total': 113, - 'blocks': { + "R13x99": { + "version_indicator": 0b10100, + "height": 13, + "width": 99, + "remainder_bits": 3, + "character_count_length": 7, + "codewords_total": 113, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 56, - 'k': 36, + "num": 1, + "c": 56, + "k": 36, }, { - 'num': 1, - 'c': 57, - 'k': 37, + "num": 1, + "c": 57, + "k": 37, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 37, - 'k': 11, + "num": 1, + "c": 37, + "k": 11, }, { - 'num': 2, - 'c': 38, - 'k': 12, + "num": 2, + "c": 38, + "k": 12, }, - ] - } + ], + }, }, - 'R13x139': { - 'version_indicator': 0b10101, - 'height': 13, - 'width': 139, - 'remainder_bits': 0, - 'character_count_length': 7, - 'codewords_total': 166, - 'blocks': { + "R13x139": { + "version_indicator": 0b10101, + "height": 13, + "width": 139, + "remainder_bits": 0, + "character_count_length": 7, + "codewords_total": 166, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 55, - 'k': 35, + "num": 2, + "c": 55, + "k": 35, }, { - 'num': 1, - 'c': 56, - 'k': 36, + "num": 1, + "c": 56, + "k": 36, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 41, - 'k': 13, + "num": 2, + "c": 41, + "k": 13, }, { - 'num': 2, - 'c': 42, - 'k': 14, + "num": 2, + "c": 42, + "k": 14, }, - ] - } + ], + }, }, - 'R15x43': { - 'version_indicator': 0b10110, - 'height': 15, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 51, - 'blocks': { + "R15x43": { + "version_indicator": 0b10110, + "height": 15, + "width": 43, + "remainder_bits": 1, + "character_count_length": 6, + "codewords_total": 51, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 51, - 'k': 33, + "num": 1, + "c": 51, + "k": 33, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 25, - 'k': 7, + "num": 1, + "c": 25, + "k": 7, }, { - 'num': 1, - 'c': 26, - 'k': 8, + "num": 1, + "c": 26, + "k": 8, }, - ] - } + ], + }, }, - 'R15x59': { - 'version_indicator': 0b10111, - 'height': 15, - 'width': 59, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 74, - 'blocks': { + "R15x59": { + "version_indicator": 0b10111, + "height": 15, + "width": 59, + "remainder_bits": 4, + "character_count_length": 6, + "codewords_total": 74, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 74, - 'k': 48, + "num": 1, + "c": 74, + "k": 48, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 37, - 'k': 13, + "num": 2, + "c": 37, + "k": 13, } - ] - } + ], + }, }, - 'R15x77': { - 'version_indicator': 0b11000, - 'height': 15, - 'width': 77, - 'remainder_bits': 6, - 'character_count_length': 7, - 'codewords_total': 103, - 'blocks': { + "R15x77": { + "version_indicator": 0b11000, + "height": 15, + "width": 77, + "remainder_bits": 6, + "character_count_length": 7, + "codewords_total": 103, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 51, - 'k': 33, + "num": 1, + "c": 51, + "k": 33, }, { - 'num': 1, - 'c': 52, - 'k': 34, + "num": 1, + "c": 52, + "k": 34, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 34, - 'k': 10, + "num": 2, + "c": 34, + "k": 10, }, { - 'num': 1, - 'c': 35, - 'k': 11, + "num": 1, + "c": 35, + "k": 11, }, - ] - } + ], + }, }, - 'R15x99': { - 'version_indicator': 0b11001, - 'height': 15, - 'width': 99, - 'remainder_bits': 7, - 'character_count_length': 7, - 'codewords_total': 136, - 'blocks': { + "R15x99": { + "version_indicator": 0b11001, + "height": 15, + "width": 99, + "remainder_bits": 7, + "character_count_length": 7, + "codewords_total": 136, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 68, - 'k': 44, + "num": 2, + "c": 68, + "k": 44, }, ], ErrorCorrectionLevel.H: [ { - 'num': 4, - 'c': 34, - 'k': 12, + "num": 4, + "c": 34, + "k": 12, }, ], - } + }, }, - 'R15x139': { - 'version_indicator': 0b11010, - 'height': 15, - 'width': 139, - 'remainder_bits': 2, - 'character_count_length': 7, - 'codewords_total': 199, - 'blocks': { + "R15x139": { + "version_indicator": 0b11010, + "height": 15, + "width": 139, + "remainder_bits": 2, + "character_count_length": 7, + "codewords_total": 199, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 66, - 'k': 42, + "num": 2, + "c": 66, + "k": 42, }, { - 'num': 1, - 'c': 67, - 'k': 43, + "num": 1, + "c": 67, + "k": 43, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 39, - 'k': 13, + "num": 1, + "c": 39, + "k": 13, }, { - 'num': 4, - 'c': 40, - 'k': 14, + "num": 4, + "c": 40, + "k": 14, }, ], - } + }, }, - 'R17x43': { - 'version_indicator': 0b11011, - 'height': 17, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 61, - 'blocks': { + "R17x43": { + "version_indicator": 0b11011, + "height": 17, + "width": 43, + "remainder_bits": 1, + "character_count_length": 6, + "codewords_total": 61, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 1, - 'c': 60, - 'k': 39, + "num": 1, + "c": 60, + "k": 39, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 30, - 'k': 10, + "num": 1, + "c": 30, + "k": 10, }, { - 'num': 1, - 'c': 31, - 'k': 11, + "num": 1, + "c": 31, + "k": 11, }, - ] - } + ], + }, }, - 'R17x59': { - 'version_indicator': 0b11100, - 'height': 17, - 'width': 59, - 'remainder_bits': 2, - 'character_count_length': 6, - 'codewords_total': 88, - 'blocks': { + "R17x59": { + "version_indicator": 0b11100, + "height": 17, + "width": 59, + "remainder_bits": 2, + "character_count_length": 6, + "codewords_total": 88, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 44, - 'k': 28, + "num": 2, + "c": 44, + "k": 28, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 44, - 'k': 14, + "num": 2, + "c": 44, + "k": 14, } - ] - } + ], + }, }, - 'R17x77': { - 'version_indicator': 0b11101, - 'height': 17, - 'width': 77, - 'remainder_bits': 0, - 'character_count_length': 7, - 'codewords_total': 122, - 'blocks': { + "R17x77": { + "version_indicator": 0b11101, + "height": 17, + "width": 77, + "remainder_bits": 0, + "character_count_length": 7, + "codewords_total": 122, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 61, - 'k': 39, + "num": 2, + "c": 61, + "k": 39, }, ], ErrorCorrectionLevel.H: [ { - 'num': 1, - 'c': 40, - 'k': 12, + "num": 1, + "c": 40, + "k": 12, }, { - 'num': 2, - 'c': 41, - 'k': 13, + "num": 2, + "c": 41, + "k": 13, }, ], - } + }, }, - 'R17x99': { - 'version_indicator': 0b11110, - 'height': 17, - 'width': 99, - 'remainder_bits': 3, - 'character_count_length': 7, - 'codewords_total': 160, - 'blocks': { + "R17x99": { + "version_indicator": 0b11110, + "height": 17, + "width": 99, + "remainder_bits": 3, + "character_count_length": 7, + "codewords_total": 160, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 2, - 'c': 53, - 'k': 33, + "num": 2, + "c": 53, + "k": 33, }, { - 'num': 1, - 'c': 54, - 'k': 34, - } + "num": 1, + "c": 54, + "k": 34, + }, ], ErrorCorrectionLevel.H: [ { - 'num': 4, - 'c': 40, - 'k': 14, + "num": 4, + "c": 40, + "k": 14, }, ], - } + }, }, - 'R17x139': { - 'version_indicator': 0b11111, - 'height': 17, - 'width': 139, - 'remainder_bits': 4, - 'character_count_length': 8, - 'codewords_total': 232, - 'blocks': { + "R17x139": { + "version_indicator": 0b11111, + "height": 17, + "width": 139, + "remainder_bits": 4, + "character_count_length": 8, + "codewords_total": 232, + "blocks": { ErrorCorrectionLevel.M: [ { - 'num': 4, - 'c': 58, - 'k': 38, + "num": 4, + "c": 58, + "k": 38, }, ], ErrorCorrectionLevel.H: [ { - 'num': 2, - 'c': 38, - 'k': 12, + "num": 2, + "c": 38, + "k": 12, }, { - 'num': 4, - 'c': 39, - 'k': 13, + "num": 4, + "c": 39, + "k": 13, }, ], - } + }, }, } diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index 31b1d07..f17d7b9 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -1,15 +1,13 @@ +from PIL import Image, ImageDraw + from .enums.color import Color -from PIL import Image -from PIL import ImageDraw class QRImage: def __init__(self, qr, module_size=10): self._module_size = module_size self._img = Image.new( - 'RGB', - ((qr.width() + 4) * module_size, (qr.height() + 4) * module_size), - (255, 255, 255) + "RGB", ((qr.width() + 4) * module_size, (qr.height() + 4) * module_size), (255, 255, 255) ) self._make_image(qr) @@ -28,7 +26,6 @@ def get_ndarray(self): def save(self, name): self._img.save(name) - def _make_image(self, qr): draw = ImageDraw.Draw(self._img) for y in range(qr.height()): @@ -37,8 +34,17 @@ def _make_image(self, qr): if qr.value_at(x, y) == Color.BLACK: r, g, b = 0, 0, 0 elif qr.value_at(x, y) == Color.WHITE: - r, g, b, = 255, 255, 255 + r, g, b, = ( + 255, + 255, + 255, + ) draw.rectangle( - xy=((x + 2) * self._module_size, (y + 2) * self._module_size, (x + 1 + 2) * self._module_size, (y + 1 + 2) * self._module_size), - fill=(r, g, b) - ) \ No newline at end of file + xy=( + (x + 2) * self._module_size, + (y + 2) * self._module_size, + (x + 1 + 2) * self._module_size, + (y + 1 + 2) * self._module_size, + ), + fill=(r, g, b), + ) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 466934b..4187484 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -1,17 +1,17 @@ -from .format.error_correction_level import ErrorCorrectionLevel -from .format.rmqr_versions import rMQRVersions -from .format.data_capacities import DataCapacities +import logging + +from .encoder.byte_encoder import ByteEncoder +from .enums.color import Color +from .enums.fit_strategy import FitStrategy from .format.alignment_pattern_coordinates import AlignmentPatternCoordinates +from .format.data_capacities import DataCapacities +from .format.error_correction_level import ErrorCorrectionLevel from .format.generator_polynomials import GeneratorPolynomials from .format.mask import mask - -from .encoder.byte_encoder import ByteEncoder +from .format.rmqr_versions import rMQRVersions from .util.error_correction import compute_bch, compute_reed_solomon from .util.utilities import split_into_8bits -from .enums.color import Color -from .enums.fit_strategy import FitStrategy -import logging class rMQR: @staticmethod @@ -22,9 +22,8 @@ def _init_logger(): logger.propagate = True return logger - @staticmethod - def fit(data,ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): + def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger = rMQR._init_logger() data_length = ByteEncoder.length(data) @@ -34,35 +33,36 @@ def fit(data,ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - if data_length <= qr_version['capacity']['Byte'][ecc]: - width, height = qr_version['width'], qr_version['height'] + if data_length <= qr_version["capacity"]["Byte"][ecc]: + width, height = qr_version["width"], qr_version["height"] if not width in determined_width and not height in determined_height: determined_width.add(width) determined_height.add(height) - ok_versions.append({ - 'version': version_name, - 'width': width, - 'height': height, - }) + ok_versions.append( + { + "version": version_name, + "width": width, + "height": height, + } + ) logger.debug(f"ok: {version_name}") if len(ok_versions) == 0: raise DataTooLongError("The data is too long.") if fit_strategy == FitStrategy.MINIMIZE_WIDTH: - sort_key = lambda x: x['width'] + sort_key = lambda x: x["width"] elif fit_strategy == FitStrategy.MINIMIZE_HEIGHT: - sort_key = lambda x: x['height'] + sort_key = lambda x: x["height"] elif fit_strategy == FitStrategy.BALANCED: - sort_key = lambda x: x['height'] * 9 + x['width'] + sort_key = lambda x: x["height"] * 9 + x["width"] selected = sorted(ok_versions, key=sort_key)[0] logger.debug(f"selected: {selected}") - qr = rMQR(selected['version'], ecc) + qr = rMQR(selected["version"], ecc) qr.make(data) return qr - def __init__(self, version, ecc, logger=None): self._logger = logger or rMQR._init_logger() @@ -71,46 +71,38 @@ def __init__(self, version, ecc, logger=None): qr_version = rMQRVersions[version] self._version = version - self._height = qr_version['height'] - self._width = qr_version['width'] + self._height = qr_version["height"] + self._width = qr_version["width"] self._error_correction_level = ecc self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] - def make(self, data): self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() self._put_timing_pattern() self._put_version_information() - mask_area = self._put_data(data); + mask_area = self._put_data(data) self._apply_mask(mask_area) - def version_name(self): return f"R{self._height}x{self._width}" - def size(self): return (self.width(), self.height()) - def height(self): return self._height - def width(self): return self._width - def value_at(self, x, y): return self._qr[y][x] - def to_list(self): return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] - def __str__(self): res = "" @@ -131,7 +123,6 @@ def __str__(self): res += "\n" return res - def _put_finder_pattern(self): # Finder pattern # Outer square @@ -145,7 +136,7 @@ def _put_finder_pattern(self): # Inner square for i in range(3): for j in range(3): - self._qr[2+i][2+j] = Color.BLACK + self._qr[2 + i][2 + j] = Color.BLACK # Separator for n in range(8): @@ -160,29 +151,27 @@ def _put_finder_pattern(self): for i in range(5): for j in range(5): color = Color.BLACK if i == 0 or i == 4 or j == 0 or j == 4 else Color.WHITE - self._qr[self._height-i-1][self._width-j-1] = color + self._qr[self._height - i - 1][self._width - j - 1] = color # Inner square - self._qr[self._height-1-2][self._width-1-2] = Color.BLACK - + self._qr[self._height - 1 - 2][self._width - 1 - 2] = Color.BLACK def _put_corner_finder_pattern(self): # Corner finder pattern # Bottom left - self._qr[self._height-1][0] = Color.BLACK - self._qr[self._height-1][1] = Color.BLACK - self._qr[self._height-1][2] = Color.BLACK + self._qr[self._height - 1][0] = Color.BLACK + self._qr[self._height - 1][1] = Color.BLACK + self._qr[self._height - 1][2] = Color.BLACK if self._height >= 11: - self._qr[self._height-2][0] = Color.BLACK - self._qr[self._height-2][1] = Color.WHITE + self._qr[self._height - 2][0] = Color.BLACK + self._qr[self._height - 2][1] = Color.WHITE # Top right - self._qr[0][self._width-1] = Color.BLACK - self._qr[0][self._width-2] = Color.BLACK - self._qr[1][self._width-1] = Color.BLACK - self._qr[1][self._width-2] = Color.WHITE - + self._qr[0][self._width - 1] = Color.BLACK + self._qr[0][self._width - 2] = Color.BLACK + self._qr[1][self._width - 1] = Color.BLACK + self._qr[1][self._width - 2] = Color.WHITE def _put_alignment_pattern(self): # Alignment pattern @@ -194,8 +183,7 @@ def _put_alignment_pattern(self): # Top side self._qr[i][center_x + j - 1] = color # Bottom side - self._qr[self._height-1-i][center_x + j - 1] = color - + self._qr[self._height - 1 - i][center_x + j - 1] = color def _put_timing_pattern(self): # Timing pattern @@ -215,13 +203,11 @@ def _put_timing_pattern(self): if self._qr[i][j] == Color.UNDEFINED: self._qr[i][j] = color - def _put_version_information(self): version_information = self._compute_version_info() self._put_version_information_finder_pattern_side(version_information) self._put_version_information_finder_sub_pattern_side(version_information) - def _put_version_information_finder_pattern_side(self, version_information): mask = 0b011111101010110010 version_information ^= mask @@ -230,8 +216,7 @@ def _put_version_information_finder_pattern_side(self, version_information): for n in range(18): di = n % 5 dj = n // 5 - self._qr[si+di][sj+dj] = Color.BLACK if version_information>>n & 1 else Color.WHITE - + self._qr[si + di][sj + dj] = Color.BLACK if version_information >> n & 1 else Color.WHITE def _put_version_information_finder_sub_pattern_side(self, version_information): mask = 0b100000101001111011 @@ -241,27 +226,31 @@ def _put_version_information_finder_sub_pattern_side(self, version_information): for n in range(15): di = n % 5 dj = n // 5 - self._qr[si+di][sj+dj] = Color.BLACK if version_information>>n & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-4] = Color.BLACK if version_information>>15 & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-3] = Color.BLACK if version_information>>16 & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-2] = Color.BLACK if version_information>>17 & 1 else Color.WHITE - + self._qr[si + di][sj + dj] = Color.BLACK if version_information >> n & 1 else Color.WHITE + self._qr[self._height - 1 - 5][self._width - 1 - 4] = ( + Color.BLACK if version_information >> 15 & 1 else Color.WHITE + ) + self._qr[self._height - 1 - 5][self._width - 1 - 3] = ( + Color.BLACK if version_information >> 16 & 1 else Color.WHITE + ) + self._qr[self._height - 1 - 5][self._width - 1 - 2] = ( + Color.BLACK if version_information >> 17 & 1 else Color.WHITE + ) def _compute_version_info(self): qr_version = rMQRVersions[self.version_name()] - version_information_data = qr_version['version_indicator'] + version_information_data = qr_version["version_indicator"] if self._error_correction_level == ErrorCorrectionLevel.H: - version_information_data |= 1<<6 + version_information_data |= 1 << 6 reminder_polynomial = compute_bch(version_information_data) - version_information_data = version_information_data<<12 | reminder_polynomial + version_information_data = version_information_data << 12 | reminder_polynomial return version_information_data - def _put_data(self, data): qr_version = rMQRVersions[self.version_name()] - character_count_length = qr_version['character_count_length'] - codewords_total = qr_version['codewords_total'] + character_count_length = qr_version["character_count_length"] + codewords_total = qr_version["codewords_total"] encoded_data = self._convert_to_bites_data(data, character_count_length, codewords_total) codewords = split_into_8bits(encoded_data) @@ -278,8 +267,7 @@ def _put_data(self, data): codewords.append("00010001") data_codewords_per_block, rs_codewords_per_block = self._split_into_blocks( - codewords, - qr_version['blocks'][self._error_correction_level] + codewords, qr_version["blocks"][self._error_correction_level] ) # Construct the final message codeword sequence @@ -301,15 +289,15 @@ def _put_data(self, data): self._logger.debug(f"Put RS data codewords {i} : {rs_codewords[i]}") # Codeword placement - dy = -1 # Up + dy = -1 # Up current_codeword_idx = 0 current_bit_idx = 0 cx, cy = self._width - 2, self._height - 6 - remainder_bits = qr_version['remainder_bits'] + remainder_bits = qr_version["remainder_bits"] mask_area = [[False for i in range(self._width)] for j in range(self._height)] while True: - for x in [cx, cx-1]: + for x in [cx, cx - 1]: if self._qr[cy][x] == Color.UNDEFINED: # Process only empty cell if current_codeword_idx == len(final_codewords): @@ -319,7 +307,11 @@ def _put_data(self, data): remainder_bits -= 1 else: # Codewords - self._qr[cy][x] = Color.BLACK if final_codewords[current_codeword_idx][current_bit_idx] == '1' else Color.WHITE + self._qr[cy][x] = ( + Color.BLACK + if final_codewords[current_codeword_idx][current_bit_idx] == "1" + else Color.WHITE + ) mask_area[cy][x] = True current_bit_idx += 1 if current_bit_idx == 8: @@ -344,15 +336,14 @@ def _put_data(self, data): return mask_area - def _split_into_blocks(self, codewords, blocks_definition): data_idx, error_idx = 0, 0 data_codewords_per_block = [] rs_codewords_per_block = [] for block_definition in blocks_definition: - for i in range(block_definition['num']): - data_codewords_num = block_definition['k'] - rs_codewords_num = block_definition['c'] - block_definition['k'] + for i in range(block_definition["num"]): + data_codewords_num = block_definition["k"] + rs_codewords_num = block_definition["c"] - block_definition["k"] g = GeneratorPolynomials[rs_codewords_num] codewords_in_block = codewords[data_idx : data_idx + data_codewords_num] @@ -366,7 +357,6 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data(self, data, character_count_length, codewords_total): encoded_data = ByteEncoder.encode(data, character_count_length) @@ -376,7 +366,6 @@ def _convert_to_bites_data(self, data, character_count_length, codewords_total): return encoded_data - def _apply_mask(self, mask_area): for y in range(self._height): for x in range(self._width): @@ -388,7 +377,6 @@ def _apply_mask(self, mask_area): elif self._qr[y][x] == Color.WHITE: self._qr[y][x] = Color.BLACK - @staticmethod def validate_version(version_name): return version_name in rMQRVersions @@ -399,4 +387,4 @@ class DataTooLongError(ValueError): class IllegalVersionError(ValueError): - pass \ No newline at end of file + pass diff --git a/src/rmqrcode/util/error_correction.py b/src/rmqrcode/util/error_correction.py index a3b47de..14943a5 100644 --- a/src/rmqrcode/util/error_correction.py +++ b/src/rmqrcode/util/error_correction.py @@ -1,12 +1,13 @@ -from .utilities import msb, to_binary from .galois_fields import GaloisFields +from .utilities import msb, to_binary + def compute_bch(data): data <<= 12 - g = 1<<12 | 1<<11 | 1<<10 | 1<<9 | 1<<8 | 1<<5 | 1<<2 | 1<<0 + g = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0 tmp_data = data - while (msb(tmp_data) >= 13): + while msb(tmp_data) >= 13: multiple = msb(tmp_data) - 13 tmp_g = g << multiple tmp_data ^= tmp_g @@ -14,6 +15,8 @@ def compute_bch(data): gf = GaloisFields() + + def compute_reed_solomon(data, g, num_error_codewords): f = list(map(lambda x: int(x, 2), data)) @@ -21,13 +24,14 @@ def compute_reed_solomon(data, g, num_error_codewords): f.append(0) for i in range(len(data)): - if f[i] == 0: continue + if f[i] == 0: + continue mult = gf.i2e[f[i]] for j in range(len(g)): - f[i+j] ^= gf.e2i[(g[j]+mult)%255] + f[i + j] ^= gf.e2i[(g[j] + mult) % 255] rs_codewords = [] for i in range(num_error_codewords): rs_codewords.append(to_binary(f[-num_error_codewords + i], 8)) - return rs_codewords \ No newline at end of file + return rs_codewords diff --git a/src/rmqrcode/util/galois_fields.py b/src/rmqrcode/util/galois_fields.py index 6b3c076..4a0cdf7 100644 --- a/src/rmqrcode/util/galois_fields.py +++ b/src/rmqrcode/util/galois_fields.py @@ -5,7 +5,7 @@ class GaloisFields: def __init__(self): # Irreducible polynomial in GF(2^8) - p = (1<<8)|(1<<4)|(1<<3)|(1<<2)|1 + p = (1 << 8) | (1 << 4) | (1 << 3) | (1 << 2) | 1 self.e2i[0] = 1 self.e2i[255] = 1 @@ -15,7 +15,7 @@ def __init__(self): tmp = 1 for e in range(1, 255): tmp <<= 1 - if tmp & (1<<8): + if tmp & (1 << 8): tmp ^= p self.e2i[e] = tmp self.i2e[tmp] = e diff --git a/src/rmqrcode/util/utilities.py b/src/rmqrcode/util/utilities.py index 8de88ac..1f4f4c4 100644 --- a/src/rmqrcode/util/utilities.py +++ b/src/rmqrcode/util/utilities.py @@ -8,9 +8,9 @@ def to_binary(data, len): def split_into_8bits(data): codewords = [] - while (len(data) >= 8): + while len(data) >= 8: codewords.append(data[:8]) data = data[8:] if data != "": - codewords.append(data.ljust(8, '0')) - return codewords \ No newline at end of file + codewords.append(data.ljust(8, "0")) + return codewords From 8442b963751aea305235ed5a96d98a5388973ef5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:39:30 +0900 Subject: [PATCH 020/113] ci: Run make lint in the python-app workflow --- .github/workflows/python-app.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c00e7dd..0d47c15 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -27,12 +27,9 @@ jobs: pip install flake8 pytest pip install . if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 + - name: Lint run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + make lint - name: Test with pytest run: | - python -m pytest + python -m pytest \ No newline at end of file From 69dff273d58d82a9c5aa81c8de8ca89e1bcc01a3 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:57:03 +0900 Subject: [PATCH 021/113] ci: flake8 --- src/rmqrcode/__init__.py | 2 ++ src/rmqrcode/console.py | 3 +-- src/rmqrcode/rmqrcode.py | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/rmqrcode/__init__.py b/src/rmqrcode/__init__.py index c537672..aca9a2f 100644 --- a/src/rmqrcode/__init__.py +++ b/src/rmqrcode/__init__.py @@ -1,3 +1,5 @@ from .format.error_correction_level import ErrorCorrectionLevel from .qr_image import QRImage from .rmqrcode import DataTooLongError, FitStrategy, IllegalVersionError, rMQR + +__all__ = ("rMQR", "DataTooLongError", "FitStrategy", "IllegalVersionError", "QRImage", "ErrorCorrectionLevel") diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index 615ce29..e02a4f8 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -2,7 +2,6 @@ import argparse import sys -import rmqrcode from rmqrcode import DataTooLongError, ErrorCorrectionLevel, FitStrategy, IllegalVersionError, QRImage, rMQR @@ -12,7 +11,7 @@ def _show_error_and_exit(msg): def _make_qr(data, ecc, version, fit_strategy): - if version == None: + if version is None: qr = rMQR.fit(data, ecc=ecc, fit_strategy=fit_strategy) else: try: diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 4187484..0aa599f 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -35,7 +35,7 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): for version_name, qr_version in DataCapacities.items(): if data_length <= qr_version["capacity"]["Byte"][ecc]: width, height = qr_version["width"], qr_version["height"] - if not width in determined_width and not height in determined_height: + if width not in determined_width and height not in determined_height: determined_width.add(width) determined_height.add(height) ok_versions.append( @@ -51,11 +51,20 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): raise DataTooLongError("The data is too long.") if fit_strategy == FitStrategy.MINIMIZE_WIDTH: - sort_key = lambda x: x["width"] + + def sort_key(x): + return x["width"] + elif fit_strategy == FitStrategy.MINIMIZE_HEIGHT: - sort_key = lambda x: x["height"] + + def sort_key(x): + return x["height"] + elif fit_strategy == FitStrategy.BALANCED: - sort_key = lambda x: x["height"] * 9 + x["width"] + + def sort_key(x): + return x["height"] * 9 + x["width"] + selected = sorted(ok_versions, key=sort_key)[0] logger.debug(f"selected: {selected}") From 81adf743456f0fac07544c36cfca0194847c6a65 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 10:59:26 +0900 Subject: [PATCH 022/113] ci: Use profile=black in isrot --- setup.cfg | 5 ++++- src/rmqrcode/console.py | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 99662bd..8360b5f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,4 +37,7 @@ console_scripts = [flake8] max-line-length = 119 extend-ignore = E203 -exclude = src/rmqrcode/format/generator_polynomials.py \ No newline at end of file +exclude = src/rmqrcode/format/generator_polynomials.py + +[isort] +profile=black diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index e02a4f8..5220c64 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -2,7 +2,14 @@ import argparse import sys -from rmqrcode import DataTooLongError, ErrorCorrectionLevel, FitStrategy, IllegalVersionError, QRImage, rMQR +from rmqrcode import ( + DataTooLongError, + ErrorCorrectionLevel, + FitStrategy, + IllegalVersionError, + QRImage, + rMQR, +) def _show_error_and_exit(msg): From a376f524a53770dd0fc4871eba71af2ac1b6ad64 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 11:02:31 +0900 Subject: [PATCH 023/113] ci: install with the dev extra --- .github/workflows/python-app.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 0d47c15..5e21ba2 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,8 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - pip install . + pip install .[dev] if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint run: | From 1f8b5d44ea4c56bf449774a301d80b968016a743 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 11:41:09 +0900 Subject: [PATCH 024/113] doc: Update CONTRIBUTING.md --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53b92d3..a0327ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,3 +11,10 @@ Consider starting commit message with one of the following prefixes. - `refactor:` : Refactoring - `chore:` : Little things - `doc:` : Documentation + + +## Pull Requests +Before make a pull request, please do the following. +1. `make format` +2. `make lint` +3. `python -m pytest` and make sure all tests pass. \ No newline at end of file From 3f9ef196682b70b78b40f60eefc5267765ff3d08 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 11:45:40 +0900 Subject: [PATCH 025/113] chore: Change CONTRIBUTING.md --- CONTRIBUTING.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0327ec..6023c90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,8 @@ # Contributing to rmqrcode-python - Thank you for interesting in contributing to rmqrcode-python! Any suggestions are welcome. ## Style Guides ### Git Commit Message - Consider starting commit message with one of the following prefixes. - `feat:` : New feature - `fix:` : Bug fix @@ -12,9 +10,8 @@ Consider starting commit message with one of the following prefixes. - `chore:` : Little things - `doc:` : Documentation - ## Pull Requests Before make a pull request, please do the following. 1. `make format` 2. `make lint` -3. `python -m pytest` and make sure all tests pass. \ No newline at end of file +3. `python -m pytest` and make sure all tests are passed. \ No newline at end of file From 7b8b325cbb0aa1103bed4a956efed756c36479a1 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 11:58:22 +0900 Subject: [PATCH 026/113] chore: Add trailing new line --- CONTRIBUTING.md | 2 +- Makefile | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6023c90..1e79947 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,4 +14,4 @@ Consider starting commit message with one of the following prefixes. Before make a pull request, please do the following. 1. `make format` 2. `make lint` -3. `python -m pytest` and make sure all tests are passed. \ No newline at end of file +3. `python -m pytest` and make sure all tests are passed. diff --git a/Makefile b/Makefile index 795901d..64421c3 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ lint: .PHONY: format format: isort src - black src \ No newline at end of file + black src diff --git a/pyproject.toml b/pyproject.toml index 9e5326f..8969d2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 119 -exclude = "generator_polynomials.py" \ No newline at end of file +exclude = "generator_polynomials.py" From afdc6c3d319b90c9dc9edadc73ada61d48e6128d Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 12:01:27 +0900 Subject: [PATCH 027/113] chore: Add trailing new line --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 5e21ba2..472aafc 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -31,4 +31,4 @@ jobs: make lint - name: Test with pytest run: | - python -m pytest \ No newline at end of file + python -m pytest From aa696a9ab4fd90d47b34dd0c0613b5df72aff653 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 28 Jul 2022 19:15:44 +0900 Subject: [PATCH 028/113] ci: Create dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..91abb11 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 68392b3a780738ccbb1473cbadfb87f70b300362 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 08:30:47 +0900 Subject: [PATCH 029/113] doc: Update the references in the README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 43f8c4d..8184e8f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ Any suggestions are welcome! If you are interesting in contiributing, please rea ## 📚 References -- [Rectangular Micro QR Code (rMQR) bar code symbology specification: ISO/IEC 23941:2022](https://www.iso.org/standard/77404.html) +- [Rectangular Micro QR Code (rMQR) bar code symbology specification: ISO/IEC 23941](https://www.iso.org/standard/77404.html) +- [rMQR Code | QRcode.com | DENSO WAVE](https://www.qrcode.com/en/codes/rmqr.html) - [Creating a QR Code step by step](https://www.nayuki.io/page/creating-a-qr-code-step-by-step) From 56592fb5837d07d4d3fb7419dda75156d14d70aa Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 10:03:27 +0900 Subject: [PATCH 030/113] feat: Add the option `with_quiet_zone` into rMQR#to_list and rMQR#__str__ --- src/rmqrcode/rmqrcode.py | 39 +++++++++++++++++++++++++++++++++------ tests/rmqrcode_test.py | 6 ++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 0aa599f..31fe70b 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -14,6 +14,8 @@ class rMQR: + QUIET_ZONE_MODULES = 2 + @staticmethod def _init_logger(): logger = logging.getLogger(__name__) @@ -72,7 +74,7 @@ def sort_key(x): qr.make(data) return qr - def __init__(self, version, ecc, logger=None): + def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() if not rMQR.validate_version(version): @@ -109,10 +111,23 @@ def width(self): def value_at(self, x, y): return self._qr[y][x] - def to_list(self): - return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] + def to_list(self, with_quiet_zone=True): + res = [] + if with_quiet_zone: + for y in range(self.QUIET_ZONE_MODULES): + res.append([0] * (self.width() + self.QUIET_ZONE_MODULES * 2)) + for row in self._to_binary_list(): + res.append([0] * self.QUIET_ZONE_MODULES + row + [0] * self.QUIET_ZONE_MODULES) + for y in range(self.QUIET_ZONE_MODULES): + res.append([0] * (self.width() + self.QUIET_ZONE_MODULES * 2)) + else: + res = self._to_binary_list() + return res + + def _to_binary_list(self): + return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] - def __str__(self): + def __str__(self, with_quiet_zone=True): res = "" show = {} @@ -123,13 +138,25 @@ def __str__(self): show[False] = "_" res += f"rMQR Version R{self._height}x{self._width}:\n" - for i in range(self._height): - for j in range(self._width): + if with_quiet_zone: + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + '\n') * self.QUIET_ZONE_MODULES + + for i in range(self.height()): + if with_quiet_zone: + res += show[False] * self.QUIET_ZONE_MODULES + + for j in range(self.width()): if self._qr[i][j] in show: res += show[self._qr[i][j]] else: res += self._qr[i][j] + + if with_quiet_zone: + res += show[False] * self.QUIET_ZONE_MODULES res += "\n" + + if with_quiet_zone: + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + '\n') * self.QUIET_ZONE_MODULES return res def _put_finder_pattern(self): diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index b857821..c075f77 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -14,6 +14,12 @@ def test_make(self): qr = rMQR('R13x99', ErrorCorrectionLevel.M) qr.make("abc") + assert len(qr.to_list(with_quiet_zone=True)) is 17 + assert len(qr.to_list(with_quiet_zone=True)[0]) is 103 + + assert len(qr.to_list(with_quiet_zone=False)) is 13 + assert len(qr.to_list(with_quiet_zone=False)[0]) is 99 + def test_raise_too_long_error(self): with pytest.raises(DataTooLongError) as e: s = "a".ljust(200, "a") From 36a7c662fd5f0e3bf5ad331cdf64b226c8dd72bd Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 10:17:06 +0900 Subject: [PATCH 031/113] chore: make format --- src/rmqrcode/rmqrcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 31fe70b..41851d5 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -125,7 +125,7 @@ def to_list(self, with_quiet_zone=True): return res def _to_binary_list(self): - return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] + return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] def __str__(self, with_quiet_zone=True): res = "" @@ -139,7 +139,7 @@ def __str__(self, with_quiet_zone=True): res += f"rMQR Version R{self._height}x{self._width}:\n" if with_quiet_zone: - res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + '\n') * self.QUIET_ZONE_MODULES + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + "\n") * self.QUIET_ZONE_MODULES for i in range(self.height()): if with_quiet_zone: @@ -156,7 +156,7 @@ def __str__(self, with_quiet_zone=True): res += "\n" if with_quiet_zone: - res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + '\n') * self.QUIET_ZONE_MODULES + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + "\n") * self.QUIET_ZONE_MODULES return res def _put_finder_pattern(self): From fbb9015aa597f060ea3c4ee1722dcc082bed421d Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 10:42:41 +0900 Subject: [PATCH 032/113] refactor: Use with-quiet-zone in QRIMage#_make_image --- src/rmqrcode/qr_image.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index f17d7b9..f62db66 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -6,10 +6,11 @@ class QRImage: def __init__(self, qr, module_size=10): self._module_size = module_size + qr_list = qr.to_list() self._img = Image.new( - "RGB", ((qr.width() + 4) * module_size, (qr.height() + 4) * module_size), (255, 255, 255) + "RGB", (len(qr_list[0]) * module_size, len(qr_list) * module_size), (255, 255, 255) ) - self._make_image(qr) + self._make_image(qr_list) def show(self): self._img.show() @@ -26,25 +27,17 @@ def get_ndarray(self): def save(self, name): self._img.save(name) - def _make_image(self, qr): + def _make_image(self, qr_list): draw = ImageDraw.Draw(self._img) - for y in range(qr.height()): - for x in range(qr.width()): - r, g, b = 125, 125, 125 - if qr.value_at(x, y) == Color.BLACK: - r, g, b = 0, 0, 0 - elif qr.value_at(x, y) == Color.WHITE: - r, g, b, = ( - 255, - 255, - 255, - ) + for y in range(len(qr_list)): + for x in range(len(qr_list[0])): + r, g, b = (0, 0, 0) if qr_list[y][x] else (255, 255, 255) draw.rectangle( xy=( - (x + 2) * self._module_size, - (y + 2) * self._module_size, - (x + 1 + 2) * self._module_size, - (y + 1 + 2) * self._module_size, + x * self._module_size, + y * self._module_size, + (x + 1) * self._module_size, + (y + 1) * self._module_size, ), fill=(r, g, b), ) From fb962fba1e04832e059719de6819a8cf0fc40366 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 10:43:12 +0900 Subject: [PATCH 033/113] chore: make format --- src/rmqrcode/qr_image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index f62db66..e8f4fd8 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -7,9 +7,7 @@ class QRImage: def __init__(self, qr, module_size=10): self._module_size = module_size qr_list = qr.to_list() - self._img = Image.new( - "RGB", (len(qr_list[0]) * module_size, len(qr_list) * module_size), (255, 255, 255) - ) + self._img = Image.new("RGB", (len(qr_list[0]) * module_size, len(qr_list) * module_size), (255, 255, 255)) self._make_image(qr_list) def show(self): From 3bd7b10178162e6b25cff4ff185fa46d0859afa1 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 4 Aug 2022 10:44:10 +0900 Subject: [PATCH 034/113] chore: Remove unused import --- src/rmqrcode/qr_image.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index e8f4fd8..a2ee637 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -1,7 +1,5 @@ from PIL import Image, ImageDraw -from .enums.color import Color - class QRImage: def __init__(self, qr, module_size=10): From 45706288ac9964887ed92ed963804e087fcf9129 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 5 Aug 2022 13:00:19 +0900 Subject: [PATCH 035/113] doc: Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8184e8f..212cb63 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![reop-url](https://user-images.githubusercontent.com/14174940/172978619-accbf9d0-9dd8-4b19-b47e-ad139a68dcc9.png) -This is an rMQR Code image generator implemented in Python. This is implemented based on [ISO/IEC 23941:2022](https://www.iso.org/standard/77404.html). +The rMQR Code is a rectangular two-dimensional barcode. This package can generate an rMQR Code image. This is implemented based on [ISO/IEC 23941: Rectangular Micro QR Code (rMQR) bar code symbology specification](https://www.iso.org/standard/77404.html). -Try this online (in Japanese): https://rmqr.oudon.xyz . +## 🎮 Online Demo Site +You can try this online: https://rmqr.oudon.xyz . - -## 📌 Important Notice +## 📌 Notice - Please verify an image generated by this software whether it can decode correctly before use. - Because this is in early stage, QR Code readers may have not been supported rMQR Code yet. From efad49567b51ddd53757624691e922e39fd760f8 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sat, 6 Aug 2022 10:24:12 +0900 Subject: [PATCH 036/113] wip: Add docstrings --- CONTRIBUTING.md | 4 ++ src/rmqrcode/rmqrcode.py | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4a2116..40429cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,10 @@ Thank you for interesting in contributing to rmqrcode-python! Any suggestions are welcome. ## Style Guides + +### Docstrings +This project uses [Google-Style format](https://google.github.io/styleguide/pyguide.html#381-docstrings) docstrings. + ### Git Commit Message Consider starting commit message with one of the following prefixes. - `feat:` : New feature diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 41851d5..cb234f0 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -18,6 +18,12 @@ class rMQR: @staticmethod def _init_logger(): + """ Initializes a logger and returns it. + + Returns: + logging.RootLogger: Logger + + """ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) logger.setLevel(logging.DEBUG) @@ -26,6 +32,20 @@ def _init_logger(): @staticmethod def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): + """ Attempts to make an rMQR have optimized version for given data. + + Args: + data (str): Data to encode. + ecc (rmqrcode.ErrorCorrectionLevel): Error correction level. + fit_strategy (rmqrcode.FitStrategy): Strategy how determine rMQR Code version. + + Returns: + rmqrcode.rMQR: Optimized rMQR Code. + + Raises: + rmqrcode.DataTooLongError: If the data is too long to encode. + + """ logger = rMQR._init_logger() data_length = ByteEncoder.length(data) @@ -97,21 +117,84 @@ def make(self, data): self._apply_mask(mask_area) def version_name(self): + """ Returns the version name. + + Returns: + str: The version name. + + Examples: + >>> qr.version_name() + "R13x77" + + """ return f"R{self._height}x{self._width}" def size(self): + """ Returns the size. + + Returns: + tuple: The rMQR Code size. + + Examples: + >>> qr.size() + (77, 13) + + Note: + This not includes the quiet zone. + + """ return (self.width(), self.height()) def height(self): + """ Returns the height. + + Returns: + int: The height. + + Note: + This not includes the quiet zone. + + """ return self._height def width(self): + """ Returns the width. + + Returns: + int: The width. + + Note: + This not includes the quiet zone. + + """ return self._width def value_at(self, x, y): + """ DEPRECATED: Returns the color at the point of (x, y). + + Returns: + rmqrcode.Color: The color of rMQRCode at the point of (x, y). + + Note: + This method is deprecated. Use to_list() alternatively. + This not includes the quiet zone. + + """ return self._qr[y][x] def to_list(self, with_quiet_zone=True): + """ Convert to two-dimensional list and returns it. + + The value is 1 for the dark module and 0 for the light module. + + Arguments: + with_quiet_zone (bool): Flag to select whether include the quiet zone. + + Returns: + list: Converted list. + + """ + res = [] if with_quiet_zone: for y in range(self.QUIET_ZONE_MODULES): From 6a3d79e648d8869cefbdc4378d3590310a625c0b Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 7 Aug 2022 08:23:09 +0900 Subject: [PATCH 037/113] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 212cb63..6602109 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ The rMQR Code is a rectangular two-dimensional barcode. This package can generate an rMQR Code image. This is implemented based on [ISO/IEC 23941: Rectangular Micro QR Code (rMQR) bar code symbology specification](https://www.iso.org/standard/77404.html). +[![pytest](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml/badge.svg?branch=main)](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml) +![PyPI](https://img.shields.io/pypi/v/rmqrcode?color=blue) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rmqrcode) +![PyPI - Downloads](https://img.shields.io/pypi/dm/rmqrcode?color=orange) + ## 🎮 Online Demo Site You can try this online: https://rmqr.oudon.xyz . From c9be13c6f6feddd9f220482e5f209668353d8ae7 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 9 Aug 2022 10:04:11 +0900 Subject: [PATCH 038/113] doc: Add docstrings to src/rmqrcode/rmqrcode.py --- src/rmqrcode/rmqrcode.py | 120 ++++++++++++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 14 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index cb234f0..a4bbdb0 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -1,3 +1,20 @@ +"""A module to make an rMQR Code. + +Example: + Use the rMQR.fit method to make an rMQR automatically with some options. + + qr = rMQR.fit( + "https://oudon.xyz", + ecc=ErrorCorrectionLevel.M, + fit_strategy=FitStrategy.MINIMIZE_WIDTH + ) + + The following example shows how to select the size of an rMQR Code. + + qr = rMQR("R11x139", ErrorCorrectionLevel.H) + qr.make("https://oudon.xyz") + +""" import logging from .encoder.byte_encoder import ByteEncoder @@ -14,11 +31,17 @@ class rMQR: + """A class to make an rMQR Code. + + Attributes: + QUIET_ZONE_MODULES (int): The width of the quiet zone. + + """ QUIET_ZONE_MODULES = 2 @staticmethod def _init_logger(): - """ Initializes a logger and returns it. + """Initializes a logger and returns it. Returns: logging.RootLogger: Logger @@ -32,10 +55,10 @@ def _init_logger(): @staticmethod def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): - """ Attempts to make an rMQR have optimized version for given data. + """Attempts to make an rMQR have optimized version for given data. Args: - data (str): Data to encode. + data (str): Data string to encode. ecc (rmqrcode.ErrorCorrectionLevel): Error correction level. fit_strategy (rmqrcode.FitStrategy): Strategy how determine rMQR Code version. @@ -108,6 +131,14 @@ def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] def make(self, data): + """Makes an rMQR Code for given data + + Args: + data (str): Data string. + + Returns: + void + """ self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() @@ -117,7 +148,7 @@ def make(self, data): self._apply_mask(mask_area) def version_name(self): - """ Returns the version name. + """Returns the version name. Returns: str: The version name. @@ -130,7 +161,7 @@ def version_name(self): return f"R{self._height}x{self._width}" def size(self): - """ Returns the size. + """Returns the size. Returns: tuple: The rMQR Code size. @@ -146,7 +177,7 @@ def size(self): return (self.width(), self.height()) def height(self): - """ Returns the height. + """Returns the height. Returns: int: The height. @@ -158,7 +189,7 @@ def height(self): return self._height def width(self): - """ Returns the width. + """Returns the width. Returns: int: The width. @@ -170,7 +201,7 @@ def width(self): return self._width def value_at(self, x, y): - """ DEPRECATED: Returns the color at the point of (x, y). + """DEPRECATED: Returns the color at the point of (x, y). Returns: rmqrcode.Color: The color of rMQRCode at the point of (x, y). @@ -183,15 +214,15 @@ def value_at(self, x, y): return self._qr[y][x] def to_list(self, with_quiet_zone=True): - """ Convert to two-dimensional list and returns it. + """Converts to two-dimensional list and returns it. - The value is 1 for the dark module and 0 for the light module. + The value is 1 for the dark module and 0 for the light module. - Arguments: - with_quiet_zone (bool): Flag to select whether include the quiet zone. + Args: + with_quiet_zone (bool): Flag to select whether include the quiet zone. - Returns: - list: Converted list. + Returns: + list: Converted list. """ @@ -208,6 +239,19 @@ def to_list(self, with_quiet_zone=True): return res def _to_binary_list(self): + """Converts to two-dimensional list and returns it. + + The value is 1 for the dark module and 0 for the light module. + + Args: + with_quiet_zone (bool): Flag to select whether include the quiet zone. + + Returns: + list: Converted list. + + Note: + This not includes the quiet zone. + """ return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] def __str__(self, with_quiet_zone=True): @@ -366,6 +410,21 @@ def _compute_version_info(self): return version_information_data def _put_data(self, data): + """Symbol character placement. + + This method puts data into the encoding region of the rMQR Code. Also this + method computes a two-dimensional list shows where encoding region at the + same time. And returns the list. + + See: "7.7.3 Symbol character placement" in the ISO/IEC 23941. + + Args: + data (str): Data string. + + Returns: + list: A two-dimensional list shows where encoding region. + + """ qr_version = rMQRVersions[self.version_name()] character_count_length = qr_version["character_count_length"] @@ -486,6 +545,18 @@ def _convert_to_bites_data(self, data, character_count_length, codewords_total): return encoded_data def _apply_mask(self, mask_area): + """Data masking. + + This method applies the data mask. + + Args: + mask_area (list): A two-dimensional list shows where encoding region. + This is computed by self._put_data(). + + Returns: + void + + """ for y in range(self._height): for x in range(self._width): if not mask_area[y][x]: @@ -498,12 +569,33 @@ def _apply_mask(self, mask_area): @staticmethod def validate_version(version_name): + """Check if the given version_name is valid + + Args: + version_name (str): Version name. + + Returns: + bool: Validation result. + + Example: + >>> rMQR.validate_version("R13x77") + True + + >>> rMQR.validate_version("R14x55") + False + + >>> rMQR.validate_version("13, 77") + False + + """ return version_name in rMQRVersions class DataTooLongError(ValueError): + "A class represents an error raised when the given data is too long." pass class IllegalVersionError(ValueError): + "A class represents an error raised when the given version name is illegal." pass From a6c54670e87d23913beb484dd4126cd93571c317 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 9 Aug 2022 18:13:46 +0900 Subject: [PATCH 039/113] chore: make format --- src/rmqrcode/rmqrcode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index a4bbdb0..5117d8e 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -15,6 +15,7 @@ qr.make("https://oudon.xyz") """ + import logging from .encoder.byte_encoder import ByteEncoder @@ -37,6 +38,7 @@ class rMQR: QUIET_ZONE_MODULES (int): The width of the quiet zone. """ + QUIET_ZONE_MODULES = 2 @staticmethod From 11c2d7e660120fef3b6039b86b98452b7e494007 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 11 Aug 2022 07:23:19 +0900 Subject: [PATCH 040/113] chore: Bumps to v1.2.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8360b5f..cf930d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = rmqrcode -version = 0.1.3 +version = 0.2.0 author = Takahiro Tomita author_email = ttp8101@gmail.com description = An rMQR Code Generetor From 3855bf3934d2e0a86f1b3500512658aa12d1272d Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 12 Aug 2022 07:13:26 +0900 Subject: [PATCH 041/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6602109..53fcaff 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![reop-url](https://user-images.githubusercontent.com/14174940/172978619-accbf9d0-9dd8-4b19-b47e-ad139a68dcc9.png) -The rMQR Code is a rectangular two-dimensional barcode. This package can generate an rMQR Code image. This is implemented based on [ISO/IEC 23941: Rectangular Micro QR Code (rMQR) bar code symbology specification](https://www.iso.org/standard/77404.html). +The rMQR Code is a rectangular two-dimensional barcode. This is easy to print in narrow space compared to conventional QR Code. This package can generate an rMQR Code image. This is implemented based on [ISO/IEC 23941: Rectangular Micro QR Code (rMQR) bar code symbology specification](https://www.iso.org/standard/77404.html). [![pytest](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml/badge.svg?branch=main)](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml) ![PyPI](https://img.shields.io/pypi/v/rmqrcode?color=blue) From a098a44a2ad1445862174d49da2cb56efc818a4c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 12 Aug 2022 08:01:31 +0900 Subject: [PATCH 042/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53fcaff..eb3e302 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# An rMQR Code Generator +# Rectangular Micro QR Code (rMQR Code) Generator ![reop-url](https://user-images.githubusercontent.com/14174940/172978619-accbf9d0-9dd8-4b19-b47e-ad139a68dcc9.png) From 901d5e07adb0f2942116f7f985f934fee0ce736b Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 11:31:28 +0900 Subject: [PATCH 043/113] feat: Use EnocderBase --- src/rmqrcode/encoder/byte_encoder.py | 29 +++++++------ src/rmqrcode/encoder/encoder_base.py | 63 ++++++++++++++++++++++++++++ src/rmqrcode/rmqrcode.py | 8 ++-- 3 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 src/rmqrcode/encoder/encoder_base.py diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index ed6bbdf..8e8abc9 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -1,21 +1,26 @@ -class ByteEncoder: - MODE_INDICATOR = "011" +from .encoder_base import EncoderBase - @staticmethod - def _encoded_bits(s): + +class ByteEncoder(EncoderBase): + @classmethod + def mode_indicator(cls): + return "011" + + @classmethod + def _encoded_bits(cls, s): res = "" encoded = s.encode("utf-8") for byte in encoded: res += bin(byte)[2:].zfill(8) return res - @staticmethod - def encode(data, character_count_length): - res = ByteEncoder.MODE_INDICATOR - res += bin(len(data))[2:].zfill(character_count_length) - res += ByteEncoder._encoded_bits(data) + @classmethod + def encode(cls, data, character_count_indicator_length): + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) return res - @staticmethod - def length(data): - return len(data.encode("utf-8")) + @classmethod + def length(cls, data, character_count_indicator_length): + return len(cls.mode_indicator()) + character_count_indicator_length + 8 * len(data.encode("utf-8")) diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py new file mode 100644 index 0000000..bbc1c43 --- /dev/null +++ b/src/rmqrcode/encoder/encoder_base.py @@ -0,0 +1,63 @@ +from abc import ABC, abstractmethod + + +class EncoderBase(ABC): + """An abstract class for encoders""" + + @classmethod + @abstractmethod + def mode_indicator(cls): + """Mode indicator defined in the Table 2. + + Returns: + str: Mode indicator like "001". + + """ + raise NotImplementedError() + + @classmethod + @abstractmethod + def encode(cls, data, character_count_indicator_length): + """Encodes data and returns it. + + Args: + data (str): Data to encode. + character_count_indicator_length: (int): Number of bits of character + count indicator defined in the Table 3. + + Returns: + str: Encoded binary as string. + + """ + raise NotImplementedError() + + @classmethod + @abstractmethod + def _encoded_bits(cls, data): + """Encodes data and returns it. + + This method encodes the raw data without the meta data like the mode + indicator, the number of data characters and so on. + + Args: + data (str): Data to encode. + + Returns: + str: Encoded binary as string. + + """ + raise NotImplementedError() + + @classmethod + @abstractmethod + def length(cls, data): + """Compute the length of the encoded bits. + + Args: + data (str): Data to encode. + + Returns: + int: The length of the encoded bits. + + """ + raise NotImplementedError() diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 5117d8e..e9138ef 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -73,13 +73,13 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): """ logger = rMQR._init_logger() - data_length = ByteEncoder.length(data) ok_versions = [] determined_width = set() determined_height = set() logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): + data_length = ByteEncoder.length(data, rMQRVersions[version_name]["character_count_length"]) if data_length <= qr_version["capacity"]["Byte"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: @@ -133,7 +133,7 @@ def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] def make(self, data): - """Makes an rMQR Code for given data + """Makes an rMQR Code for given data. Args: data (str): Data string. @@ -429,9 +429,9 @@ def _put_data(self, data): """ qr_version = rMQRVersions[self.version_name()] - character_count_length = qr_version["character_count_length"] + character_count_indicator_length = qr_version["character_count_length"] codewords_total = qr_version["codewords_total"] - encoded_data = self._convert_to_bites_data(data, character_count_length, codewords_total) + encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total) codewords = split_into_8bits(encoded_data) if len(codewords) > codewords_total: From 9481e982c44b5fee7b130a2f18a033e3ebeaaf08 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 11:51:31 +0900 Subject: [PATCH 044/113] doc: Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index eb3e302..2a68784 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Alphanumeric|| |Byte|✅| |Kanji|| +|Mixed|| ## 🤝 Contiributing From ccc038942ba91cc8c29444621fcca9eae2b34f86 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 11:54:00 +0900 Subject: [PATCH 045/113] doc: Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb3e302..f856b14 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Kanji|| -## 🤝 Contiributing +## 🤝 Contributing Any suggestions are welcome! If you are interesting in contiributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). From e6638e69828916e54da79786b16e284895f3b085 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 11:54:37 +0900 Subject: [PATCH 046/113] doc: Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f856b14..2f763ec 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji ## 🤝 Contributing -Any suggestions are welcome! If you are interesting in contiributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). +Any suggestions are welcome! If you are interesting in contributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). ## 📚 References From 29997baa9d7f4367f706e1a708cf50fc0f7a1adc Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 20:18:39 +0900 Subject: [PATCH 047/113] feat: Add the length of character_count_length for ByteEncoder --- src/rmqrcode/encoder/__init__.py | 4 + src/rmqrcode/encoder/numeric_encoder.py | 19 +++ src/rmqrcode/format/rmqr_versions.py | 162 +++++++++++++++++++----- src/rmqrcode/rmqrcode.py | 16 ++- 4 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 src/rmqrcode/encoder/numeric_encoder.py diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index e69de29..0f6cb04 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -0,0 +1,4 @@ +from .byte_encoder import ByteEncoder +from .numeric_encoder import NumericEncoder + +__all__ = ("ByteEncoder", "NumericEncoder") diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py new file mode 100644 index 0000000..f727b6a --- /dev/null +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -0,0 +1,19 @@ +from .encoder_base import EncoderBase + + +class NumericEncoder(EncoderBase): + @classmethod + def mode_indicator(cls): + return "001" + + @classmethod + def _encoded_bits(cls, s): + raise NotImplementedError() + + @classmethod + def encode(cls, data, character_count_indicator_length): + raise NotImplementedError() + + @classmethod + def length(cls, data, character_count_indicator_length): + raise NotImplementedError() diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index 6861772..a5be24a 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -1,4 +1,6 @@ from .error_correction_level import ErrorCorrectionLevel +from ..encoder import NumericEncoder, ByteEncoder + rMQRVersions = { "R7x43": { @@ -6,7 +8,10 @@ "height": 7, "width": 43, "remainder_bits": 0, - "character_count_length": 3, + "character_count_length": { + ByteEncoder: 3, + NumericEncoder: 4, + }, "codewords_total": 13, "blocks": { ErrorCorrectionLevel.M: [ @@ -30,7 +35,10 @@ "height": 7, "width": 59, "remainder_bits": 3, - "character_count_length": 4, + "character_count_length": { + NumericEncoder: 5, + ByteEncoder: 4, + }, "codewords_total": 21, "blocks": { ErrorCorrectionLevel.M: [ @@ -54,7 +62,10 @@ "height": 7, "width": 77, "remainder_bits": 5, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 6, + ByteEncoder: 5, + }, "codewords_total": 32, "blocks": { ErrorCorrectionLevel.M: [ @@ -78,7 +89,10 @@ "height": 7, "width": 99, "remainder_bits": 6, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 5, + }, "codewords_total": 44, "blocks": { ErrorCorrectionLevel.M: [ @@ -102,7 +116,10 @@ "height": 7, "width": 139, "remainder_bits": 1, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 68, "blocks": { ErrorCorrectionLevel.M: [ @@ -126,7 +143,10 @@ "height": 9, "width": 43, "remainder_bits": 2, - "character_count_length": 4, + "character_count_length": { + NumericEncoder: 5, + ByteEncoder: 4, + }, "codewords_total": 21, "blocks": { ErrorCorrectionLevel.M: [ @@ -150,7 +170,10 @@ "height": 9, "width": 59, "remainder_bits": 3, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 6, + ByteEncoder: 5, + }, "codewords_total": 33, "blocks": { ErrorCorrectionLevel.M: [ @@ -174,7 +197,10 @@ "height": 9, "width": 77, "remainder_bits": 1, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 5, + }, "codewords_total": 49, "blocks": { ErrorCorrectionLevel.M: [ @@ -203,7 +229,10 @@ "height": 9, "width": 99, "remainder_bits": 4, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 66, "blocks": { ErrorCorrectionLevel.M: [ @@ -227,7 +256,10 @@ "height": 9, "width": 139, "remainder_bits": 5, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 6, + }, "codewords_total": 99, "blocks": { ErrorCorrectionLevel.M: [ @@ -256,7 +288,10 @@ "height": 11, "width": 27, "remainder_bits": 2, - "character_count_length": 3, + "character_count_length": { + NumericEncoder: 4, + ByteEncoder: 3, + }, "codewords_total": 15, "blocks": { ErrorCorrectionLevel.M: [ @@ -280,7 +315,10 @@ "height": 11, "width": 43, "remainder_bits": 1, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 6, + ByteEncoder: 5, + }, "codewords_total": 31, "blocks": { ErrorCorrectionLevel.M: [ @@ -304,7 +342,10 @@ "height": 11, "width": 59, "remainder_bits": 0, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 5, + }, "codewords_total": 47, "blocks": { ErrorCorrectionLevel.M: [ @@ -333,7 +374,10 @@ "height": 11, "width": 77, "remainder_bits": 2, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 67, "blocks": { ErrorCorrectionLevel.M: [ @@ -362,7 +406,10 @@ "height": 11, "width": 99, "remainder_bits": 7, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 6, + }, "codewords_total": 89, "blocks": { ErrorCorrectionLevel.M: [ @@ -396,7 +443,10 @@ "height": 11, "width": 139, "remainder_bits": 6, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 132, "blocks": { ErrorCorrectionLevel.M: [ @@ -419,7 +469,10 @@ "version_indicator": 0b10000, "height": 13, "width": 27, - "character_count_length": 4, + "character_count_length": { + NumericEncoder: 5, + ByteEncoder: 4, + }, "remainder_bits": 4, "codewords_total": 21, "blocks": { @@ -444,7 +497,10 @@ "height": 13, "width": 43, "remainder_bits": 1, - "character_count_length": 5, + "character_count_length": { + NumericEncoder: 6, + ByteEncoder: 5, + }, "codewords_total": 41, "blocks": { ErrorCorrectionLevel.M: [ @@ -468,7 +524,10 @@ "height": 13, "width": 59, "remainder_bits": 6, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 60, "blocks": { ErrorCorrectionLevel.M: [ @@ -492,7 +551,10 @@ "height": 13, "width": 77, "remainder_bits": 4, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 85, "blocks": { ErrorCorrectionLevel.M: [ @@ -526,7 +588,10 @@ "height": 13, "width": 99, "remainder_bits": 3, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 113, "blocks": { ErrorCorrectionLevel.M: [ @@ -560,7 +625,10 @@ "height": 13, "width": 139, "remainder_bits": 0, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 166, "blocks": { ErrorCorrectionLevel.M: [ @@ -594,7 +662,10 @@ "height": 15, "width": 43, "remainder_bits": 1, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 51, "blocks": { ErrorCorrectionLevel.M: [ @@ -623,7 +694,10 @@ "height": 15, "width": 59, "remainder_bits": 4, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 74, "blocks": { ErrorCorrectionLevel.M: [ @@ -647,7 +721,10 @@ "height": 15, "width": 77, "remainder_bits": 6, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 103, "blocks": { ErrorCorrectionLevel.M: [ @@ -681,7 +758,10 @@ "height": 15, "width": 99, "remainder_bits": 7, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 136, "blocks": { ErrorCorrectionLevel.M: [ @@ -705,7 +785,10 @@ "height": 15, "width": 139, "remainder_bits": 2, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 9, + ByteEncoder: 7, + }, "codewords_total": 199, "blocks": { ErrorCorrectionLevel.M: [ @@ -739,7 +822,10 @@ "height": 17, "width": 43, "remainder_bits": 1, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 7, + ByteEncoder: 6, + }, "codewords_total": 61, "blocks": { ErrorCorrectionLevel.M: [ @@ -768,7 +854,10 @@ "height": 17, "width": 59, "remainder_bits": 2, - "character_count_length": 6, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 6, + }, "codewords_total": 88, "blocks": { ErrorCorrectionLevel.M: [ @@ -792,7 +881,10 @@ "height": 17, "width": 77, "remainder_bits": 0, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 122, "blocks": { ErrorCorrectionLevel.M: [ @@ -821,7 +913,10 @@ "height": 17, "width": 99, "remainder_bits": 3, - "character_count_length": 7, + "character_count_length": { + NumericEncoder: 8, + ByteEncoder: 7, + }, "codewords_total": 160, "blocks": { ErrorCorrectionLevel.M: [ @@ -850,7 +945,10 @@ "height": 17, "width": 139, "remainder_bits": 4, - "character_count_length": 8, + "character_count_length": { + NumericEncoder: 9, + ByteEncoder: 8, + }, "codewords_total": 232, "blocks": { ErrorCorrectionLevel.M: [ diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index e9138ef..2a0f346 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -77,9 +77,12 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): determined_width = set() determined_height = set() + # Fixed value currently + encoder = ByteEncoder + logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - data_length = ByteEncoder.length(data, rMQRVersions[version_name]["character_count_length"]) + data_length = encoder.length(data, rMQRVersions[version_name]["character_count_length"][encoder]) if data_length <= qr_version["capacity"]["Byte"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: @@ -429,9 +432,12 @@ def _put_data(self, data): """ qr_version = rMQRVersions[self.version_name()] - character_count_indicator_length = qr_version["character_count_length"] + # Fixed value currently + encoder = ByteEncoder + + character_count_indicator_length = qr_version["character_count_length"][encoder] codewords_total = qr_version["codewords_total"] - encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total) + encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total, encoder) codewords = split_into_8bits(encoded_data) if len(codewords) > codewords_total: @@ -537,8 +543,8 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data(self, data, character_count_length, codewords_total): - encoded_data = ByteEncoder.encode(data, character_count_length) + def _convert_to_bites_data(self, data, character_count_length, codewords_total, encoder): + encoded_data = encoder.encode(data, character_count_length) # Terminator (may be truncated) if len(encoded_data) + 3 <= codewords_total * 8: From 2cd0c2be4a06664d8fe5dcba330dd4f9855dc633 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 20:22:42 +0900 Subject: [PATCH 048/113] refactor: Rename from character_count_length to character_count_indicator_length --- src/rmqrcode/format/rmqr_versions.py | 64 ++++++++++++++-------------- src/rmqrcode/rmqrcode.py | 8 ++-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index a5be24a..f301796 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -8,7 +8,7 @@ "height": 7, "width": 43, "remainder_bits": 0, - "character_count_length": { + "character_count_indicator_length": { ByteEncoder: 3, NumericEncoder: 4, }, @@ -35,7 +35,7 @@ "height": 7, "width": 59, "remainder_bits": 3, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 5, ByteEncoder: 4, }, @@ -62,7 +62,7 @@ "height": 7, "width": 77, "remainder_bits": 5, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 6, ByteEncoder: 5, }, @@ -89,7 +89,7 @@ "height": 7, "width": 99, "remainder_bits": 6, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 5, }, @@ -116,7 +116,7 @@ "height": 7, "width": 139, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -143,7 +143,7 @@ "height": 9, "width": 43, "remainder_bits": 2, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 5, ByteEncoder: 4, }, @@ -170,7 +170,7 @@ "height": 9, "width": 59, "remainder_bits": 3, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 6, ByteEncoder: 5, }, @@ -197,7 +197,7 @@ "height": 9, "width": 77, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 5, }, @@ -229,7 +229,7 @@ "height": 9, "width": 99, "remainder_bits": 4, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -256,7 +256,7 @@ "height": 9, "width": 139, "remainder_bits": 5, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 6, }, @@ -288,7 +288,7 @@ "height": 11, "width": 27, "remainder_bits": 2, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 4, ByteEncoder: 3, }, @@ -315,7 +315,7 @@ "height": 11, "width": 43, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 6, ByteEncoder: 5, }, @@ -342,7 +342,7 @@ "height": 11, "width": 59, "remainder_bits": 0, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 5, }, @@ -374,7 +374,7 @@ "height": 11, "width": 77, "remainder_bits": 2, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -406,7 +406,7 @@ "height": 11, "width": 99, "remainder_bits": 7, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 6, }, @@ -443,7 +443,7 @@ "height": 11, "width": 139, "remainder_bits": 6, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -469,7 +469,7 @@ "version_indicator": 0b10000, "height": 13, "width": 27, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 5, ByteEncoder: 4, }, @@ -497,7 +497,7 @@ "height": 13, "width": 43, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 6, ByteEncoder: 5, }, @@ -524,7 +524,7 @@ "height": 13, "width": 59, "remainder_bits": 6, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -551,7 +551,7 @@ "height": 13, "width": 77, "remainder_bits": 4, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -588,7 +588,7 @@ "height": 13, "width": 99, "remainder_bits": 3, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -625,7 +625,7 @@ "height": 13, "width": 139, "remainder_bits": 0, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -662,7 +662,7 @@ "height": 15, "width": 43, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -694,7 +694,7 @@ "height": 15, "width": 59, "remainder_bits": 4, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -721,7 +721,7 @@ "height": 15, "width": 77, "remainder_bits": 6, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -758,7 +758,7 @@ "height": 15, "width": 99, "remainder_bits": 7, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -785,7 +785,7 @@ "height": 15, "width": 139, "remainder_bits": 2, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 9, ByteEncoder: 7, }, @@ -822,7 +822,7 @@ "height": 17, "width": 43, "remainder_bits": 1, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 7, ByteEncoder: 6, }, @@ -854,7 +854,7 @@ "height": 17, "width": 59, "remainder_bits": 2, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 6, }, @@ -881,7 +881,7 @@ "height": 17, "width": 77, "remainder_bits": 0, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -913,7 +913,7 @@ "height": 17, "width": 99, "remainder_bits": 3, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 8, ByteEncoder: 7, }, @@ -945,7 +945,7 @@ "height": 17, "width": 139, "remainder_bits": 4, - "character_count_length": { + "character_count_indicator_length": { NumericEncoder: 9, ByteEncoder: 8, }, diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 2a0f346..d784dee 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -82,7 +82,7 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - data_length = encoder.length(data, rMQRVersions[version_name]["character_count_length"][encoder]) + data_length = encoder.length(data, rMQRVersions[version_name]["character_count_indicator_length"][encoder]) if data_length <= qr_version["capacity"]["Byte"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: @@ -435,7 +435,7 @@ def _put_data(self, data): # Fixed value currently encoder = ByteEncoder - character_count_indicator_length = qr_version["character_count_length"][encoder] + character_count_indicator_length = qr_version["character_count_indicator_length"][encoder] codewords_total = qr_version["codewords_total"] encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total, encoder) codewords = split_into_8bits(encoded_data) @@ -543,8 +543,8 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data(self, data, character_count_length, codewords_total, encoder): - encoded_data = encoder.encode(data, character_count_length) + def _convert_to_bites_data(self, data, character_count_indicator_length, codewords_total, encoder): + encoded_data = encoder.encode(data, character_count_indicator_length) # Terminator (may be truncated) if len(encoded_data) + 3 <= codewords_total * 8: From 014c5c9f3f95e277df2cced86a43862f4419956b Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 14 Aug 2022 20:30:59 +0900 Subject: [PATCH 049/113] ci: Add the command to Makefile --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 64421c3..c92130f 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +.PHONY: test +test: + python -m pytest + .PHONY: lint lint: flake8 src From 18edc750e7ed1abdc0b0c8d1bf145ae593d867e6 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 07:41:52 +0900 Subject: [PATCH 050/113] feat: Implement NumericEncoder --- Makefile | 8 ++++++ src/rmqrcode/encoder/byte_encoder.py | 14 +++++----- src/rmqrcode/encoder/numeric_encoder.py | 37 +++++++++++++++++++++---- tests/numeric_encoder_test.py | 13 +++++++++ 4 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 tests/numeric_encoder_test.py diff --git a/Makefile b/Makefile index c92130f..985623c 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +.PHONY: install +install: + pip install -e ".[dev]" + +.PHONY: uninstall +uninstall: + yes Y | pip uninstall rmqrcode + .PHONY: test test: python -m pytest diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index 8e8abc9..858a232 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -6,6 +6,13 @@ class ByteEncoder(EncoderBase): def mode_indicator(cls): return "011" + @classmethod + def encode(cls, data, character_count_indicator_length): + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res + @classmethod def _encoded_bits(cls, s): res = "" @@ -14,13 +21,6 @@ def _encoded_bits(cls, s): res += bin(byte)[2:].zfill(8) return res - @classmethod - def encode(cls, data, character_count_indicator_length): - res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) - res += cls._encoded_bits(data) - return res - @classmethod def length(cls, data, character_count_indicator_length): return len(cls.mode_indicator()) + character_count_indicator_length + 8 * len(data.encode("utf-8")) diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index f727b6a..b4b9137 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -1,3 +1,4 @@ +import re from .encoder_base import EncoderBase @@ -7,13 +8,39 @@ def mode_indicator(cls): return "001" @classmethod - def _encoded_bits(cls, s): - raise NotImplementedError() + def encode(cls, data, character_count_indicator_length): + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res @classmethod - def encode(cls, data, character_count_indicator_length): - raise NotImplementedError() + def _encoded_bits(cls, data): + res = "" + data_grouped = cls._group_by_3characters(data) + for num in data_grouped: + if len(num) == 3: + res += bin(int(num))[2:].zfill(10) + elif len(num) == 2: + res += bin(int(num))[2:].zfill(7) + elif len(num) == 1: + res += bin(int(num))[2:].zfill(4) + return res + + @classmethod + def _group_by_3characters(cls, data): + res = [] + while data != "": + res.append(data[:3]) + data = data[3:] + return res @classmethod def length(cls, data, character_count_indicator_length): - raise NotImplementedError() + if len(data) % 3 == 0: + r = 0 + elif len(data) % 3 == 1: + r = 4 + elif len(data) % 3 == 2: + r = 7 + return len(cls.mode_indicator()) + character_count_indicator_length + 10 * (len(data) // 3) + r diff --git a/tests/numeric_encoder_test.py b/tests/numeric_encoder_test.py new file mode 100644 index 0000000..11246e4 --- /dev/null +++ b/tests/numeric_encoder_test.py @@ -0,0 +1,13 @@ +from rmqrcode.encoder import NumericEncoder + +import pytest + + +class TestNumericEncoder: + def test_encode(self): + encoded = NumericEncoder.encode("0123456789012345", 5) + assert encoded == "00110000000000110001010110011010100110111000010100111010100101" + + def test_length(self): + encoded_length = NumericEncoder.length("0123456789012345", 5) + assert encoded_length is 62 \ No newline at end of file From ded632a9022f31191d01a15f4870a15668cb4525 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 08:13:28 +0900 Subject: [PATCH 051/113] feat: Raises IllegalCharacterError in NumericalEncoder.encode --- src/rmqrcode/encoder/__init__.py | 3 ++- src/rmqrcode/encoder/encoder_base.py | 22 ++++++++++++++++++++++ src/rmqrcode/encoder/numeric_encoder.py | 12 +++++++++++- tests/numeric_encoder_test.py | 12 ++++++++++-- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index 0f6cb04..2992c6d 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -1,4 +1,5 @@ from .byte_encoder import ByteEncoder from .numeric_encoder import NumericEncoder +from .encoder_base import IllegalCharacterError -__all__ = ("ByteEncoder", "NumericEncoder") +__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError") diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index bbc1c43..ebfa414 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -28,6 +28,9 @@ def encode(cls, data, character_count_indicator_length): Returns: str: Encoded binary as string. + Raises: + IllegalCharacterError: If the data includes illegal character. + """ raise NotImplementedError() @@ -61,3 +64,22 @@ def length(cls, data): """ raise NotImplementedError() + + @classmethod + @abstractmethod + def is_valid_characters(cls, data): + """Checks wether the data does not include invalid character. + + Args: + data (str): Data to validate. + + Returns: + bool: Validation result. + + """ + raise NotImplementedError() + + +class IllegalCharacterError(ValueError): + "A class represents an error raised when the given data includes illegal character." + pass \ No newline at end of file diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index b4b9137..dd99fe8 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -1,5 +1,5 @@ import re -from .encoder_base import EncoderBase +from .encoder_base import EncoderBase, IllegalCharacterError class NumericEncoder(EncoderBase): @@ -9,6 +9,9 @@ def mode_indicator(cls): @classmethod def encode(cls, data, character_count_indicator_length): + if not cls.is_valid_characters(data): + raise IllegalCharacterError + res = cls.mode_indicator() res += bin(len(data))[2:].zfill(character_count_indicator_length) res += cls._encoded_bits(data) @@ -44,3 +47,10 @@ def length(cls, data, character_count_indicator_length): elif len(data) % 3 == 2: r = 7 return len(cls.mode_indicator()) + character_count_indicator_length + 10 * (len(data) // 3) + r + + @classmethod + def is_valid_characters(cls, data): + for c in data: + if ord(c) < ord('0') or ord(c) > ord('9'): + return False + return True \ No newline at end of file diff --git a/tests/numeric_encoder_test.py b/tests/numeric_encoder_test.py index 11246e4..e4947ec 100644 --- a/tests/numeric_encoder_test.py +++ b/tests/numeric_encoder_test.py @@ -1,4 +1,4 @@ -from rmqrcode.encoder import NumericEncoder +from rmqrcode.encoder import NumericEncoder, IllegalCharacterError import pytest @@ -8,6 +8,14 @@ def test_encode(self): encoded = NumericEncoder.encode("0123456789012345", 5) assert encoded == "00110000000000110001010110011010100110111000010100111010100101" + def test_encode_raises_invalid_character_error(self): + with pytest.raises(IllegalCharacterError) as e: + NumericEncoder.encode("ABC123", 5) + def test_length(self): encoded_length = NumericEncoder.length("0123456789012345", 5) - assert encoded_length is 62 \ No newline at end of file + assert encoded_length is 62 + + def test_is_valid_characters(self): + assert NumericEncoder.is_valid_characters("0123456789") is True + assert NumericEncoder.is_valid_characters("A1234!678@") is False \ No newline at end of file From 86ea5162b62fc61ff110eec4bbdf4623377ab151 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 08:14:48 +0900 Subject: [PATCH 052/113] chore: Fix lint --- src/rmqrcode/encoder/encoder_base.py | 2 +- src/rmqrcode/encoder/numeric_encoder.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index ebfa414..4155cb5 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -82,4 +82,4 @@ def is_valid_characters(cls, data): class IllegalCharacterError(ValueError): "A class represents an error raised when the given data includes illegal character." - pass \ No newline at end of file + pass diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index dd99fe8..d09cc77 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -1,4 +1,3 @@ -import re from .encoder_base import EncoderBase, IllegalCharacterError @@ -53,4 +52,4 @@ def is_valid_characters(cls, data): for c in data: if ord(c) < ord('0') or ord(c) > ord('9'): return False - return True \ No newline at end of file + return True From bfe9dcc93b63f1fa658c3c6e2b78e7015dec04e1 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 08:15:41 +0900 Subject: [PATCH 053/113] chore: Run make format --- src/rmqrcode/encoder/__init__.py | 2 +- src/rmqrcode/encoder/numeric_encoder.py | 2 +- src/rmqrcode/format/rmqr_versions.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index 2992c6d..966c72a 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -1,5 +1,5 @@ from .byte_encoder import ByteEncoder -from .numeric_encoder import NumericEncoder from .encoder_base import IllegalCharacterError +from .numeric_encoder import NumericEncoder __all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError") diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index d09cc77..8606a69 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -50,6 +50,6 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): for c in data: - if ord(c) < ord('0') or ord(c) > ord('9'): + if ord(c) < ord("0") or ord(c) > ord("9"): return False return True diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index f301796..c0a8aeb 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -1,6 +1,5 @@ +from ..encoder import ByteEncoder, NumericEncoder from .error_correction_level import ErrorCorrectionLevel -from ..encoder import NumericEncoder, ByteEncoder - rMQRVersions = { "R7x43": { From ef85bff0aca11b553113e7b6b378098e4ed4e43c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 08:17:21 +0900 Subject: [PATCH 054/113] chore: Add newline to last line --- tests/numeric_encoder_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/numeric_encoder_test.py b/tests/numeric_encoder_test.py index e4947ec..460ae78 100644 --- a/tests/numeric_encoder_test.py +++ b/tests/numeric_encoder_test.py @@ -18,4 +18,4 @@ def test_length(self): def test_is_valid_characters(self): assert NumericEncoder.is_valid_characters("0123456789") is True - assert NumericEncoder.is_valid_characters("A1234!678@") is False \ No newline at end of file + assert NumericEncoder.is_valid_characters("A1234!678@") is False From 0fbcdf3bd8b4f229b27f1e99a4605fcd750f783a Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 08:30:54 +0900 Subject: [PATCH 055/113] feat: Add a method ByteEncoder.is_valid_characters --- src/rmqrcode/encoder/byte_encoder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index 858a232..538325c 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -24,3 +24,7 @@ def _encoded_bits(cls, s): @classmethod def length(cls, data, character_count_indicator_length): return len(cls.mode_indicator()) + character_count_indicator_length + 8 * len(data.encode("utf-8")) + + @classmethod + def is_valid_characters(cls, data): + return True # Any characters can encode in the Byte Mode \ No newline at end of file From 74f87f458d44c235c4d2a4f2eb131e7515dc539b Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 09:43:39 +0900 Subject: [PATCH 056/113] feat: Add the argument `encoder_class` into rMQR.make --- example.py | 10 ++++++---- src/rmqrcode/rmqrcode.py | 29 ++++++++++++++++------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/example.py b/example.py index 16f7eb2..f866a67 100644 --- a/example.py +++ b/example.py @@ -2,6 +2,7 @@ from rmqrcode import ErrorCorrectionLevel from rmqrcode import QRImage from rmqrcode import FitStrategy +from rmqrcode import encoder import logging @@ -20,13 +21,14 @@ def main(): fit_strategy = FitStrategy.BALANCED # Determine rMQR version automatically - qr = rMQR.fit(data, ecc=error_correction_level, fit_strategy=fit_strategy) - print(qr) + # qr = rMQR.fit(data, ecc=error_correction_level, fit_strategy=fit_strategy) + # print(qr) # Determine rMQR version manually - # version = 'R13x99' + # version = 'R7x43' # qr = rMQR(version, error_correction_level) - # qr.make(data) + # Also you can select encoding mode manually + # qr.make("123456", encoder_class=encoder.NumericEncoder) # print(qr) # Save as png diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index d784dee..8237e9d 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -18,7 +18,7 @@ import logging -from .encoder.byte_encoder import ByteEncoder +from . import encoder from .enums.color import Color from .enums.fit_strategy import FitStrategy from .format.alignment_pattern_coordinates import AlignmentPatternCoordinates @@ -78,11 +78,11 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): determined_height = set() # Fixed value currently - encoder = ByteEncoder + encoder_class = encoder.ByteEncoder logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - data_length = encoder.length(data, rMQRVersions[version_name]["character_count_indicator_length"][encoder]) + data_length = encoder_class.length(data, rMQRVersions[version_name]["character_count_indicator_length"][encoder_class]) if data_length <= qr_version["capacity"]["Byte"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: @@ -135,21 +135,24 @@ def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._error_correction_level = ecc self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] - def make(self, data): + def make(self, data, encoder_class=encoder.ByteEncoder): """Makes an rMQR Code for given data. Args: data (str): Data string. + encoder_class (abc.ABCMeta): Pass a subclass of EncoderBase to select encoding mode. + Using ByteEncoder by default. Returns: void + """ self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() self._put_timing_pattern() self._put_version_information() - mask_area = self._put_data(data) + mask_area = self._put_data(data, encoder_class=encoder_class) self._apply_mask(mask_area) def version_name(self): @@ -414,7 +417,7 @@ def _compute_version_info(self): version_information_data = version_information_data << 12 | reminder_polynomial return version_information_data - def _put_data(self, data): + def _put_data(self, data, encoder_class=encoder.ByteEncoder): """Symbol character placement. This method puts data into the encoding region of the rMQR Code. Also this @@ -425,6 +428,8 @@ def _put_data(self, data): Args: data (str): Data string. + encoder_class (abc.ABCMeta): Pass a subclass of EncoderBase to select encoding mode. + Using ByteEncoder by default. Returns: list: A two-dimensional list shows where encoding region. @@ -432,13 +437,11 @@ def _put_data(self, data): """ qr_version = rMQRVersions[self.version_name()] - # Fixed value currently - encoder = ByteEncoder - - character_count_indicator_length = qr_version["character_count_indicator_length"][encoder] + character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] codewords_total = qr_version["codewords_total"] - encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total, encoder) + encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total, encoder_class=encoder_class) codewords = split_into_8bits(encoded_data) + print(codewords) if len(codewords) > codewords_total: raise DataTooLongError("The data is too long.") @@ -543,8 +546,8 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data(self, data, character_count_indicator_length, codewords_total, encoder): - encoded_data = encoder.encode(data, character_count_indicator_length) + def _convert_to_bites_data(self, data, character_count_indicator_length, codewords_total, encoder_class=encoder.ByteEncoder): + encoded_data = encoder_class.encode(data, character_count_indicator_length) # Terminator (may be truncated) if len(encoded_data) + 3 <= codewords_total * 8: From 821b0ce52d18aaaab64960d8927a7ef6ef055d17 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 15:43:46 +0900 Subject: [PATCH 057/113] feat: Add number_of_data_bits to DataCapacities --- example.py | 4 +- src/rmqrcode/encoder/byte_encoder.py | 2 +- src/rmqrcode/format/data_capacities.py | 128 +++++++++++++++++++++++++ src/rmqrcode/rmqrcode.py | 14 ++- 4 files changed, 141 insertions(+), 7 deletions(-) diff --git a/example.py b/example.py index f866a67..35a9f84 100644 --- a/example.py +++ b/example.py @@ -21,8 +21,8 @@ def main(): fit_strategy = FitStrategy.BALANCED # Determine rMQR version automatically - # qr = rMQR.fit(data, ecc=error_correction_level, fit_strategy=fit_strategy) - # print(qr) + qr = rMQR.fit(data, ecc=error_correction_level, fit_strategy=fit_strategy) + print(qr) # Determine rMQR version manually # version = 'R7x43' diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index 538325c..a65ca0d 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -27,4 +27,4 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): - return True # Any characters can encode in the Byte Mode \ No newline at end of file + return True # Any characters can encode in the Byte Mode diff --git a/src/rmqrcode/format/data_capacities.py b/src/rmqrcode/format/data_capacities.py index 304f067..f67f154 100644 --- a/src/rmqrcode/format/data_capacities.py +++ b/src/rmqrcode/format/data_capacities.py @@ -5,6 +5,10 @@ "R7x43": { "height": 7, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 48, + ErrorCorrectionLevel.H: 24, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 5, @@ -15,6 +19,10 @@ "R7x59": { "height": 7, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 11, @@ -25,6 +33,10 @@ "R7x77": { "height": 7, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 160, + ErrorCorrectionLevel.H: 80, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 19, @@ -35,6 +47,10 @@ "R7x99": { "height": 7, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 224, + ErrorCorrectionLevel.H: 112, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 27, @@ -45,6 +61,10 @@ "R7x139": { "height": 7, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 352, + ErrorCorrectionLevel.H: 192, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 42, @@ -55,6 +75,10 @@ "R9x43": { "height": 9, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 11, @@ -65,6 +89,10 @@ "R9x59": { "height": 9, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 168, + ErrorCorrectionLevel.H: 88, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 20, @@ -75,6 +103,10 @@ "R9x77": { "height": 9, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 248, + ErrorCorrectionLevel.H: 136, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 30, @@ -85,6 +117,10 @@ "R9x99": { "height": 9, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 336, + ErrorCorrectionLevel.H: 176, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 40, @@ -95,6 +131,10 @@ "R9x139": { "height": 9, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 504, + ErrorCorrectionLevel.H: 264, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 61, @@ -105,6 +145,10 @@ "R11x27": { "height": 11, "width": 27, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 56, + ErrorCorrectionLevel.H: 40, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 6, @@ -115,6 +159,10 @@ "R11x43": { "height": 11, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 152, + ErrorCorrectionLevel.H: 88, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 18, @@ -125,6 +173,10 @@ "R11x59": { "height": 11, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 248, + ErrorCorrectionLevel.H: 120, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 30, @@ -135,6 +187,10 @@ "R11x77": { "height": 11, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 344, + ErrorCorrectionLevel.H: 184, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 41, @@ -145,6 +201,10 @@ "R11x99": { "height": 11, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 456, + ErrorCorrectionLevel.H: 232, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 55, @@ -155,6 +215,10 @@ "R11x139": { "height": 11, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 672, + ErrorCorrectionLevel.H: 336, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 82, @@ -165,6 +229,10 @@ "R13x27": { "height": 13, "width": 27, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 11, @@ -175,6 +243,10 @@ "R13x43": { "height": 13, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 216, + ErrorCorrectionLevel.H: 104, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 26, @@ -185,6 +257,10 @@ "R13x59": { "height": 13, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 304, + ErrorCorrectionLevel.H: 160, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 36, @@ -195,6 +271,10 @@ "R13x77": { "height": 13, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 424, + ErrorCorrectionLevel.H: 232, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 51, @@ -205,6 +285,10 @@ "R13x99": { "height": 13, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 584, + ErrorCorrectionLevel.H: 280, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 71, @@ -215,6 +299,10 @@ "R13x139": { "height": 13, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 848, + ErrorCorrectionLevel.H: 232, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 104, @@ -225,6 +313,10 @@ "R15x43": { "height": 15, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 264, + ErrorCorrectionLevel.H: 120, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 31, @@ -235,6 +327,10 @@ "R15x59": { "height": 15, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 384, + ErrorCorrectionLevel.H: 208, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 46, @@ -245,6 +341,10 @@ "R15x77": { "height": 15, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 536, + ErrorCorrectionLevel.H: 248, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 65, @@ -255,6 +355,10 @@ "R15x99": { "height": 15, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 704, + ErrorCorrectionLevel.H: 384, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 86, @@ -265,6 +369,10 @@ "R15x139": { "height": 15, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 1016, + ErrorCorrectionLevel.H: 552, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 125, @@ -275,6 +383,10 @@ "R17x43": { "height": 17, "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 312, + ErrorCorrectionLevel.H: 168, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 37, @@ -285,6 +397,10 @@ "R17x59": { "height": 17, "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 448, + ErrorCorrectionLevel.H: 224, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 54, @@ -295,6 +411,10 @@ "R17x77": { "height": 17, "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 624, + ErrorCorrectionLevel.H: 304, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 76, @@ -305,6 +425,10 @@ "R17x99": { "height": 17, "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 800, + ErrorCorrectionLevel.H: 448, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 98, @@ -315,6 +439,10 @@ "R17x139": { "height": 17, "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 1216, + ErrorCorrectionLevel.H: 608, + }, "capacity": { "Byte": { ErrorCorrectionLevel.M: 150, diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 8237e9d..13477c5 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -82,8 +82,10 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - data_length = encoder_class.length(data, rMQRVersions[version_name]["character_count_indicator_length"][encoder_class]) - if data_length <= qr_version["capacity"]["Byte"][ecc]: + data_length = encoder_class.length( + data, rMQRVersions[version_name]["character_count_indicator_length"][encoder_class] + ) + if data_length <= qr_version["number_of_data_bits"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: determined_width.add(width) @@ -439,7 +441,9 @@ def _put_data(self, data, encoder_class=encoder.ByteEncoder): character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] codewords_total = qr_version["codewords_total"] - encoded_data = self._convert_to_bites_data(data, character_count_indicator_length, codewords_total, encoder_class=encoder_class) + encoded_data = self._convert_to_bites_data( + data, character_count_indicator_length, codewords_total, encoder_class=encoder_class + ) codewords = split_into_8bits(encoded_data) print(codewords) @@ -546,7 +550,9 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data(self, data, character_count_indicator_length, codewords_total, encoder_class=encoder.ByteEncoder): + def _convert_to_bites_data( + self, data, character_count_indicator_length, codewords_total, encoder_class=encoder.ByteEncoder + ): encoded_data = encoder_class.encode(data, character_count_indicator_length) # Terminator (may be truncated) From f67eeb3a55817f51fa49e585cf6e1e8e0af797f0 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 15:44:47 +0900 Subject: [PATCH 058/113] fix: Fix number_of_data_bits --- src/rmqrcode/format/data_capacities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmqrcode/format/data_capacities.py b/src/rmqrcode/format/data_capacities.py index f67f154..adeb1d7 100644 --- a/src/rmqrcode/format/data_capacities.py +++ b/src/rmqrcode/format/data_capacities.py @@ -301,7 +301,7 @@ "width": 139, "number_of_data_bits": { ErrorCorrectionLevel.M: 848, - ErrorCorrectionLevel.H: 232, + ErrorCorrectionLevel.H: 432, }, "capacity": { "Byte": { From 4cc1a395e20758d4e82d45b8712a0884c45f8bbc Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 15 Aug 2022 15:56:18 +0900 Subject: [PATCH 059/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c606648..ee7283c 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Mode|Supported?| |-|:-:| -|Numeric| | +|Numeric|✅| |Alphanumeric|| |Byte|✅| |Kanji|| From e57325c84d9f97264f98fe6b7704d5ba7477e500 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 16 Aug 2022 07:42:05 +0900 Subject: [PATCH 060/113] feat: Implement AlphanumericEncoder --- README.md | 2 +- src/rmqrcode/encoder/__init__.py | 3 +- src/rmqrcode/encoder/alphanumeric_encoder.py | 99 ++++++++++++++++++++ tests/alphanumeric_encoder_test.py | 21 +++++ 4 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/rmqrcode/encoder/alphanumeric_encoder.py create mode 100644 tests/alphanumeric_encoder_test.py diff --git a/README.md b/README.md index ee7283c..6c983b2 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Mode|Supported?| |-|:-:| |Numeric|✅| -|Alphanumeric|| +|Alphanumeric|✅| |Byte|✅| |Kanji|| |Mixed|| diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index 966c72a..e56f306 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -1,5 +1,6 @@ +from .alphanumeric_encoder import AlphanumericEncoder from .byte_encoder import ByteEncoder from .encoder_base import IllegalCharacterError from .numeric_encoder import NumericEncoder -__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError") +__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError", "AlphanumericEncoder") diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py new file mode 100644 index 0000000..f2689a2 --- /dev/null +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -0,0 +1,99 @@ +from .encoder_base import EncoderBase, IllegalCharacterError + + +class AlphanumericEncoder(EncoderBase): + CHARACTER_MAP = { + "0": 0, + "1": 1, + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "A": 10, + "B": 11, + "C": 12, + "D": 13, + "E": 14, + "F": 15, + "G": 16, + "H": 17, + "I": 18, + "J": 19, + "K": 20, + "L": 21, + "M": 22, + "N": 23, + "O": 24, + "P": 25, + "Q": 26, + "R": 27, + "S": 28, + "T": 29, + "U": 30, + "V": 31, + "W": 32, + "X": 33, + "Y": 34, + "Z": 35, + " ": 36, + "$": 37, + "%": 38, + "*": 39, + "+": 40, + "-": 41, + ".": 42, + "/": 43, + ":": 44, + } + + @classmethod + def mode_indicator(cls): + return "010" + + @classmethod + def encode(cls, data, character_count_indicator_length): + if not cls.is_valid_characters(data): + raise IllegalCharacterError + + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res + + @classmethod + def _encoded_bits(cls, data): + res = "" + data_grouped = cls._group_by_2characters(data) + for s in data_grouped: + if len(s) == 2: + value = cls.CHARACTER_MAP[s[0]] * 45 + cls.CHARACTER_MAP[s[1]] + res += bin(value)[2:].zfill(11) + elif len(s) == 1: + value = cls.CHARACTER_MAP[s[0]] + res += bin(value)[2:].zfill(6) + return res + + @classmethod + def _group_by_2characters(cls, data): + res = [] + while data != "": + res.append(data[:2]) + data = data[2:] + return res + + @classmethod + def length(cls, data, character_count_indicator_length): + return ( + len(cls.mode_indicator()) + character_count_indicator_length + 11 * (len(data) // 2) + 6 * (len(data) % 2) + ) + + @classmethod + def is_valid_characters(cls, data): + for c in data: + if c not in cls.CHARACTER_MAP: + return False + return True diff --git a/tests/alphanumeric_encoder_test.py b/tests/alphanumeric_encoder_test.py new file mode 100644 index 0000000..c9576f4 --- /dev/null +++ b/tests/alphanumeric_encoder_test.py @@ -0,0 +1,21 @@ +from rmqrcode.encoder import AlphanumericEncoder, IllegalCharacterError + +import pytest + + +class TestAlphaNumericEncoder: + def test_encode(self): + encoded = AlphanumericEncoder.encode("AC-42", 5) + assert encoded == "010001010011100111011100111001000010" + + def test_encode_raises_invalid_character_error(self): + with pytest.raises(IllegalCharacterError) as e: + AlphanumericEncoder.encode("abc123", 5) + + def test_length(self): + encoded_length = AlphanumericEncoder.length("AC-42", 5) + assert encoded_length is 36 + + def test_is_valid_characters(self): + assert AlphanumericEncoder.is_valid_characters("AC-42") is True + assert AlphanumericEncoder.is_valid_characters("abc123") is False From c8d42949e953db6b4d679057ea4683892297b6c5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 16 Aug 2022 08:06:51 +0900 Subject: [PATCH 061/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c983b2..096ce02 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ qr.make("https://oudon.xyz") ## 🛠️ Under the Hood ### Encoding modes -The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. However, this package supoprts only Byte mode currently. +The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. For now, the supported encoding modes are below. |Mode|Supported?| |-|:-:| From a5f62f83ed25c78c90d9ef9bb9974992577da95f Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:25:51 +0900 Subject: [PATCH 062/113] feat: Implement KanjiEncoder --- src/rmqrcode/encoder/__init__.py | 3 +- src/rmqrcode/encoder/kanji_encoder.py | 52 +++++++++++++++++++++++++++ tests/kanji_encoder_test.py | 21 +++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/rmqrcode/encoder/kanji_encoder.py create mode 100644 tests/kanji_encoder_test.py diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index e56f306..0df2cee 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -2,5 +2,6 @@ from .byte_encoder import ByteEncoder from .encoder_base import IllegalCharacterError from .numeric_encoder import NumericEncoder +from .kanji_encoder import KanjiEncoder -__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError", "AlphanumericEncoder") +__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError", "AlphanumericEncoder", "KanjiEncoder") diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py new file mode 100644 index 0000000..5ac7d27 --- /dev/null +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -0,0 +1,52 @@ +from .encoder_base import EncoderBase, IllegalCharacterError + + +class KanjiEncoder(EncoderBase): + @classmethod + def mode_indicator(cls): + return "100" + + @classmethod + def encode(cls, data, character_count_indicator_length): + if not cls.is_valid_characters(data): + raise IllegalCharacterError + + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res + + @classmethod + def _encoded_bits(cls, data): + res = "" + for c in data: + shift_jis = c.encode('shift-jis') + hex_value = shift_jis[0] * 256 + shift_jis[1] + + if hex_value >= 0x8140 and hex_value <= 0x9FFC: + sub = 0x8140 + elif hex_value >= 0xE040 and hex_value <= 0xEBBF: + sub = 0xC140 + else: + raise IllegalCharacterError() + + msb = (hex_value - sub) >> 8 + lsb = (hex_value - sub) & 255 + encoded_value = msb * 0xC0 + lsb + res += bin(encoded_value)[2:].zfill(13) + return res + + @classmethod + def length(cls, data, character_count_indicator_length): + return len(cls.mode_indicator()) + character_count_indicator_length + 13 * len(data) + + @classmethod + def is_valid_characters(cls, data): + for c in data: + shift_jis = c.encode('shift_jis') + if len(shift_jis) < 2: + return False + hex_value = shift_jis[0] * 256 + shift_jis[1] + if (0x8140 > hex_value and 0x9ffc < hex_value) or (0xe040 > hex_value and 0xebbf < hex_value): + return False + return True diff --git a/tests/kanji_encoder_test.py b/tests/kanji_encoder_test.py new file mode 100644 index 0000000..4c0e626 --- /dev/null +++ b/tests/kanji_encoder_test.py @@ -0,0 +1,21 @@ +from rmqrcode.encoder import KanjiEncoder, IllegalCharacterError + +import pytest + + +class TestKanjiEncoder: + def test_encode(self): + encoded = KanjiEncoder.encode("点茗", 5) + assert encoded == "1000001001101100111111101010101010" + + def test_encode_raises_invalid_character_error(self): + with pytest.raises(IllegalCharacterError) as e: + KanjiEncoder.encode("abc123", 5) + + def test_length(self): + encoded_length = KanjiEncoder.length("点茗", 5) + assert encoded_length is 34 + + def test_is_valid_characters(self): + assert KanjiEncoder.is_valid_characters("点茗") is True + assert KanjiEncoder.is_valid_characters("abc") is False From 09cab6c186246194e0ff5c06096f49cd0a1afca0 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:26:24 +0900 Subject: [PATCH 063/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 096ce02..1d13085 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji |Numeric|✅| |Alphanumeric|✅| |Byte|✅| -|Kanji|| +|Kanji|✅| |Mixed|| From 6c0e963c992afdc11758c7f17884d0c83c87e94f Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:42:44 +0900 Subject: [PATCH 064/113] feat: Add character_count_indicator_length for AlphaNumeric and ByteEncoder --- src/rmqrcode/format/rmqr_versions.py | 66 +++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index c0a8aeb..358e91e 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -8,8 +8,10 @@ "width": 43, "remainder_bits": 0, "character_count_indicator_length": { - ByteEncoder: 3, NumericEncoder: 4, + AlphanumericEncoder: 3, + ByteEncoder: 3, + KanjiEncoder: 2, }, "codewords_total": 13, "blocks": { @@ -36,7 +38,9 @@ "remainder_bits": 3, "character_count_indicator_length": { NumericEncoder: 5, + AlphanumericEncoder: 5, ByteEncoder: 4, + KanjiEncoder: 3, }, "codewords_total": 21, "blocks": { @@ -63,7 +67,9 @@ "remainder_bits": 5, "character_count_indicator_length": { NumericEncoder: 6, + AlphanumericEncoder: 5, ByteEncoder: 5, + KanjiEncoder: 4, }, "codewords_total": 32, "blocks": { @@ -90,7 +96,9 @@ "remainder_bits": 6, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 5, + KanjiEncoder: 5, }, "codewords_total": 44, "blocks": { @@ -117,7 +125,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 68, "blocks": { @@ -144,7 +154,9 @@ "remainder_bits": 2, "character_count_indicator_length": { NumericEncoder: 5, + AlphanumericEncoder: 5, ByteEncoder: 4, + KanjiEncoder: 3, }, "codewords_total": 21, "blocks": { @@ -171,7 +183,9 @@ "remainder_bits": 3, "character_count_indicator_length": { NumericEncoder: 6, + AlphanumericEncoder: 5, ByteEncoder: 5, + KanjiEncoder: 4, }, "codewords_total": 33, "blocks": { @@ -198,7 +212,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 5, + KanjiEncoder: 5, }, "codewords_total": 49, "blocks": { @@ -230,7 +246,9 @@ "remainder_bits": 4, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 66, "blocks": { @@ -257,7 +275,9 @@ "remainder_bits": 5, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 6, + KanjiEncoder: 6, }, "codewords_total": 99, "blocks": { @@ -289,7 +309,9 @@ "remainder_bits": 2, "character_count_indicator_length": { NumericEncoder: 4, + AlphanumericEncoder: 4, ByteEncoder: 3, + KanjiEncoder: 2, }, "codewords_total": 15, "blocks": { @@ -316,7 +338,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 6, + AlphanumericEncoder: 5, ByteEncoder: 5, + KanjiEncoder: 4, }, "codewords_total": 31, "blocks": { @@ -343,7 +367,9 @@ "remainder_bits": 0, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 5, + KanjiEncoder: 5, }, "codewords_total": 47, "blocks": { @@ -375,7 +401,9 @@ "remainder_bits": 2, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 67, "blocks": { @@ -407,7 +435,9 @@ "remainder_bits": 7, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 6, + KanjiEncoder: 6, }, "codewords_total": 89, "blocks": { @@ -444,7 +474,9 @@ "remainder_bits": 6, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 132, "blocks": { @@ -470,7 +502,9 @@ "width": 27, "character_count_indicator_length": { NumericEncoder: 5, + AlphanumericEncoder: 5, ByteEncoder: 4, + KanjiEncoder: 3, }, "remainder_bits": 4, "codewords_total": 21, @@ -498,7 +532,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 6, + AlphanumericEncoder: 6, ByteEncoder: 5, + KanjiEncoder: 5, }, "codewords_total": 41, "blocks": { @@ -525,7 +561,9 @@ "remainder_bits": 6, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 60, "blocks": { @@ -552,7 +590,9 @@ "remainder_bits": 4, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 7, ByteEncoder: 6, + KanjiEncoder: 6, }, "codewords_total": 85, "blocks": { @@ -589,7 +629,9 @@ "remainder_bits": 3, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 113, "blocks": { @@ -626,7 +668,9 @@ "remainder_bits": 0, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 8, ByteEncoder: 7, + KanjiEncoder: 7, }, "codewords_total": 166, "blocks": { @@ -663,7 +707,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 51, "blocks": { @@ -695,7 +741,9 @@ "remainder_bits": 4, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 7, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 74, "blocks": { @@ -722,7 +770,9 @@ "remainder_bits": 6, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 103, "blocks": { @@ -759,7 +809,9 @@ "remainder_bits": 7, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 136, "blocks": { @@ -786,7 +838,9 @@ "remainder_bits": 2, "character_count_indicator_length": { NumericEncoder: 9, + AlphanumericEncoder: 8, ByteEncoder: 7, + KanjiEncoder: 7, }, "codewords_total": 199, "blocks": { @@ -823,7 +877,9 @@ "remainder_bits": 1, "character_count_indicator_length": { NumericEncoder: 7, + AlphanumericEncoder: 6, ByteEncoder: 6, + KanjiEncoder: 5, }, "codewords_total": 61, "blocks": { @@ -855,7 +911,9 @@ "remainder_bits": 2, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 6, + KanjiEncoder: 6, }, "codewords_total": 88, "blocks": { @@ -882,7 +940,9 @@ "remainder_bits": 0, "character_count_indicator_length": { NumericEncoder: 8, + AlphanumericEncoder: 7, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 122, "blocks": { @@ -914,7 +974,9 @@ "remainder_bits": 3, "character_count_indicator_length": { NumericEncoder: 8, + KanjiEncoder: 8, ByteEncoder: 7, + KanjiEncoder: 6, }, "codewords_total": 160, "blocks": { @@ -946,7 +1008,9 @@ "remainder_bits": 4, "character_count_indicator_length": { NumericEncoder: 9, + AlphanumericEncoder: 8, ByteEncoder: 8, + KanjiEncoder: 7, }, "codewords_total": 232, "blocks": { From f46a3ccf6f7d7765869241f5abc8ca4ce6fe90ca Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:44:05 +0900 Subject: [PATCH 065/113] fix: Fix wrong key --- src/rmqrcode/encoder/__init__.py | 2 +- src/rmqrcode/encoder/kanji_encoder.py | 6 +++--- src/rmqrcode/format/rmqr_versions.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index 0df2cee..3ed1e17 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -1,7 +1,7 @@ from .alphanumeric_encoder import AlphanumericEncoder from .byte_encoder import ByteEncoder from .encoder_base import IllegalCharacterError -from .numeric_encoder import NumericEncoder from .kanji_encoder import KanjiEncoder +from .numeric_encoder import NumericEncoder __all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError", "AlphanumericEncoder", "KanjiEncoder") diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py index 5ac7d27..21d337a 100644 --- a/src/rmqrcode/encoder/kanji_encoder.py +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -20,7 +20,7 @@ def encode(cls, data, character_count_indicator_length): def _encoded_bits(cls, data): res = "" for c in data: - shift_jis = c.encode('shift-jis') + shift_jis = c.encode("shift-jis") hex_value = shift_jis[0] * 256 + shift_jis[1] if hex_value >= 0x8140 and hex_value <= 0x9FFC: @@ -43,10 +43,10 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): for c in data: - shift_jis = c.encode('shift_jis') + shift_jis = c.encode("shift_jis") if len(shift_jis) < 2: return False hex_value = shift_jis[0] * 256 + shift_jis[1] - if (0x8140 > hex_value and 0x9ffc < hex_value) or (0xe040 > hex_value and 0xebbf < hex_value): + if (0x8140 > hex_value and 0x9FFC < hex_value) or (0xE040 > hex_value and 0xEBBF < hex_value): return False return True diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py index 358e91e..c35c472 100644 --- a/src/rmqrcode/format/rmqr_versions.py +++ b/src/rmqrcode/format/rmqr_versions.py @@ -1,4 +1,4 @@ -from ..encoder import ByteEncoder, NumericEncoder +from ..encoder import AlphanumericEncoder, ByteEncoder, KanjiEncoder, NumericEncoder from .error_correction_level import ErrorCorrectionLevel rMQRVersions = { @@ -974,7 +974,7 @@ "remainder_bits": 3, "character_count_indicator_length": { NumericEncoder: 8, - KanjiEncoder: 8, + AlphanumericEncoder: 8, ByteEncoder: 7, KanjiEncoder: 6, }, From 97374d4b365a577ecaa16040c5b4a27dd9a8be57 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:52:45 +0900 Subject: [PATCH 066/113] refactor: Move encoder tests to tests/encoder dir --- tests/{ => encoder}/alphanumeric_encoder_test.py | 0 tests/{ => encoder}/kanji_encoder_test.py | 0 tests/{ => encoder}/numeric_encoder_test.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => encoder}/alphanumeric_encoder_test.py (100%) rename tests/{ => encoder}/kanji_encoder_test.py (100%) rename tests/{ => encoder}/numeric_encoder_test.py (100%) diff --git a/tests/alphanumeric_encoder_test.py b/tests/encoder/alphanumeric_encoder_test.py similarity index 100% rename from tests/alphanumeric_encoder_test.py rename to tests/encoder/alphanumeric_encoder_test.py diff --git a/tests/kanji_encoder_test.py b/tests/encoder/kanji_encoder_test.py similarity index 100% rename from tests/kanji_encoder_test.py rename to tests/encoder/kanji_encoder_test.py diff --git a/tests/numeric_encoder_test.py b/tests/encoder/numeric_encoder_test.py similarity index 100% rename from tests/numeric_encoder_test.py rename to tests/encoder/numeric_encoder_test.py From 791e3eed9f566592bac2fc728773e681543035ff Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 08:56:14 +0900 Subject: [PATCH 067/113] refactor: Move the implement of FooEncoder.encode into BaseEncoder.encode --- src/rmqrcode/encoder/alphanumeric_encoder.py | 10 ---------- src/rmqrcode/encoder/byte_encoder.py | 7 ------- src/rmqrcode/encoder/encoder_base.py | 8 +++++++- src/rmqrcode/encoder/kanji_encoder.py | 10 ---------- src/rmqrcode/encoder/numeric_encoder.py | 10 ---------- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py index f2689a2..2bd1c30 100644 --- a/src/rmqrcode/encoder/alphanumeric_encoder.py +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -54,16 +54,6 @@ class AlphanumericEncoder(EncoderBase): def mode_indicator(cls): return "010" - @classmethod - def encode(cls, data, character_count_indicator_length): - if not cls.is_valid_characters(data): - raise IllegalCharacterError - - res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) - res += cls._encoded_bits(data) - return res - @classmethod def _encoded_bits(cls, data): res = "" diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index a65ca0d..2f707e5 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -6,13 +6,6 @@ class ByteEncoder(EncoderBase): def mode_indicator(cls): return "011" - @classmethod - def encode(cls, data, character_count_indicator_length): - res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) - res += cls._encoded_bits(data) - return res - @classmethod def _encoded_bits(cls, s): res = "" diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index 4155cb5..cedfc07 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -32,7 +32,13 @@ def encode(cls, data, character_count_indicator_length): IllegalCharacterError: If the data includes illegal character. """ - raise NotImplementedError() + if not cls.is_valid_characters(data): + raise IllegalCharacterError + + res = cls.mode_indicator() + res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res @classmethod @abstractmethod diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py index 21d337a..8346807 100644 --- a/src/rmqrcode/encoder/kanji_encoder.py +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -6,16 +6,6 @@ class KanjiEncoder(EncoderBase): def mode_indicator(cls): return "100" - @classmethod - def encode(cls, data, character_count_indicator_length): - if not cls.is_valid_characters(data): - raise IllegalCharacterError - - res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) - res += cls._encoded_bits(data) - return res - @classmethod def _encoded_bits(cls, data): res = "" diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index 8606a69..5749170 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -6,16 +6,6 @@ class NumericEncoder(EncoderBase): def mode_indicator(cls): return "001" - @classmethod - def encode(cls, data, character_count_indicator_length): - if not cls.is_valid_characters(data): - raise IllegalCharacterError - - res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) - res += cls._encoded_bits(data) - return res - @classmethod def _encoded_bits(cls, data): res = "" From f0e5eb6b776a7b32af09764ba5db5c2bc2890a47 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 09:09:50 +0900 Subject: [PATCH 068/113] refactor: Use regular expression in is_valid_characters --- src/rmqrcode/encoder/alphanumeric_encoder.py | 9 ++++----- src/rmqrcode/encoder/numeric_encoder.py | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py index 2bd1c30..f6cb9f3 100644 --- a/src/rmqrcode/encoder/alphanumeric_encoder.py +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -1,4 +1,6 @@ -from .encoder_base import EncoderBase, IllegalCharacterError +import re + +from .encoder_base import EncoderBase class AlphanumericEncoder(EncoderBase): @@ -83,7 +85,4 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): - for c in data: - if c not in cls.CHARACTER_MAP: - return False - return True + return bool(re.match(r"^[A-Z\s\$\%\*\+\-\.\/\:]*$", data)) diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index 5749170..f9f8a8e 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -1,4 +1,6 @@ -from .encoder_base import EncoderBase, IllegalCharacterError +import re + +from .encoder_base import EncoderBase class NumericEncoder(EncoderBase): @@ -39,7 +41,4 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): - for c in data: - if ord(c) < ord("0") or ord(c) > ord("9"): - return False - return True + return bool(re.match(r"^[0-9]*$", data)) From 431b403fe02f413b7a111343039b2d86b72575df Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 09:15:37 +0900 Subject: [PATCH 069/113] fix: Fix regexp --- src/rmqrcode/encoder/alphanumeric_encoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py index f6cb9f3..53e0c41 100644 --- a/src/rmqrcode/encoder/alphanumeric_encoder.py +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -85,4 +85,4 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): - return bool(re.match(r"^[A-Z\s\$\%\*\+\-\.\/\:]*$", data)) + return bool(re.match(r"^[0-9A-Z\s\$\%\*\+\-\.\/\:]*$", data)) From d6073f4c96e1b55e479336fd1ce312ad3037e439 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 11:46:43 +0900 Subject: [PATCH 070/113] doc: Update README.md --- README.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1d13085..32ea921 100644 --- a/README.md +++ b/README.md @@ -103,19 +103,26 @@ qr.make("https://oudon.xyz") |R15|❌|✅|✅|✅|✅|✅| |R17|❌|✅|✅|✅|✅|✅| - -## 🛠️ Under the Hood ### Encoding modes -The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. For now, the supported encoding modes are below. +The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. The following example shows how to encode data "123456" in the Numeric mode. We can select an encoding mode by passing encoder_class argument to the `rMQR#make` method. In this case, the length of bits after encoding is 27 in the Numeric mode, which is shorter than 56 in the Byte mode. + +```py +from rmqrcode import rMQR, ErrorCorrectionLevel, encoder +qr = rMQR('R13x43', ErrorCorrectionLevel.M) +qr.make("123456", encoder_class=encoder.NumericEncoder) +``` + +The value for `encoder_class` is listed in the below table. + +|Mode|Value of encoder_class|Characters| +|-|-|-| +|Numeric|NumericEncoder|0-9| +|Alphanumeric|AlphanumericEncoder|0-9 A-Z \s $ % * + - . / :| +|Byte|ByteEncoder|Any| +|Kanji|KanjiEncoder|from 0x8140 to 0x9FFC, from 0xE040 to 0xEBBF in Shift JIS value| -|Mode|Supported?| -|-|:-:| -|Numeric|✅| -|Alphanumeric|✅| -|Byte|✅| -|Kanji|✅| -|Mixed|| +The rMQR Code also supports mixed modes in order to encode more efficiently. However, this package has not supported this feature yet. ## 🤝 Contributing From dbc9311622bc8c30625d5c39e44975dab36e0a1c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 11:50:26 +0900 Subject: [PATCH 071/113] fix: Add encoder into rmqrcode/__init__.py --- src/rmqrcode/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rmqrcode/__init__.py b/src/rmqrcode/__init__.py index aca9a2f..ec4cf23 100644 --- a/src/rmqrcode/__init__.py +++ b/src/rmqrcode/__init__.py @@ -1,5 +1,6 @@ from .format.error_correction_level import ErrorCorrectionLevel from .qr_image import QRImage from .rmqrcode import DataTooLongError, FitStrategy, IllegalVersionError, rMQR +from . import encoder -__all__ = ("rMQR", "DataTooLongError", "FitStrategy", "IllegalVersionError", "QRImage", "ErrorCorrectionLevel") +__all__ = ("rMQR", "DataTooLongError", "FitStrategy", "IllegalVersionError", "QRImage", "ErrorCorrectionLevel", "encoder") From 70e132bf6134c5ae76ed06e3af7a40b50afcc8ea Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Wed, 17 Aug 2022 11:52:00 +0900 Subject: [PATCH 072/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32ea921..de6605e 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ qr.make("https://oudon.xyz") ### Encoding modes -The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. The following example shows how to encode data "123456" in the Numeric mode. We can select an encoding mode by passing encoder_class argument to the `rMQR#make` method. In this case, the length of bits after encoding is 27 in the Numeric mode, which is shorter than 56 in the Byte mode. +The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. The following example shows how to encode data "123456" in the Numeric mode. We can select an encoding mode by passing the `encoder_class` argument to the `rMQR#make` method. In this case, the length of bits after encoding is 27 in the Numeric mode, which is shorter than 56 in the Byte mode. ```py from rmqrcode import rMQR, ErrorCorrectionLevel, encoder From 30fca848fbe843567758b9945e6f0cc5b6b10692 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 10:48:45 +0900 Subject: [PATCH 073/113] feat: Supports multiple segments --- src/rmqrcode/__init__.py | 21 +++++++-- src/rmqrcode/console.py | 4 +- src/rmqrcode/rmqrcode.py | 94 +++++++++++++++++++++++----------------- tests/rmqrcode_test.py | 11 ++++- 4 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/rmqrcode/__init__.py b/src/rmqrcode/__init__.py index ec4cf23..1e05407 100644 --- a/src/rmqrcode/__init__.py +++ b/src/rmqrcode/__init__.py @@ -1,6 +1,21 @@ +from . import encoder from .format.error_correction_level import ErrorCorrectionLevel from .qr_image import QRImage -from .rmqrcode import DataTooLongError, FitStrategy, IllegalVersionError, rMQR -from . import encoder +from .rmqrcode import ( + DataTooLongError, + FitStrategy, + IllegalVersionError, + NoSegmentError, + rMQR, +) -__all__ = ("rMQR", "DataTooLongError", "FitStrategy", "IllegalVersionError", "QRImage", "ErrorCorrectionLevel", "encoder") +__all__ = ( + "rMQR", + "DataTooLongError", + "FitStrategy", + "IllegalVersionError", + "NoSegmentError", + "QRImage", + "ErrorCorrectionLevel", + "encoder", +) diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py index 5220c64..66c7bfc 100644 --- a/src/rmqrcode/console.py +++ b/src/rmqrcode/console.py @@ -25,8 +25,8 @@ def _make_qr(data, ecc, version, fit_strategy): qr = rMQR(version, ecc) except IllegalVersionError: _show_error_and_exit("Error: Illegal version.") - qr.make(data) - + qr.add_segment(data) + qr.make() return qr diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 13477c5..5d57315 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -121,7 +121,8 @@ def sort_key(x): logger.debug(f"selected: {selected}") qr = rMQR(selected["version"], ecc) - qr.make(data) + qr.add_segment(data, encoder_class) + qr.make() return qr def __init__(self, version, ecc, with_quiet_zone=True, logger=None): @@ -136,27 +137,56 @@ def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._width = qr_version["width"] self._error_correction_level = ecc self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] + self._segments = [] - def make(self, data, encoder_class=encoder.ByteEncoder): - """Makes an rMQR Code for given data. + def add_segment(self, data, encoder_class=encoder.ByteEncoder): + self._segments.append({"data": data, "encoder_class": encoder_class}) - Args: - data (str): Data string. - encoder_class (abc.ABCMeta): Pass a subclass of EncoderBase to select encoding mode. - Using ByteEncoder by default. + def make(self): + """Makes an rMQR Code for stored segments. + + This method makes an rMQR Code for stored segments. Before call this, + you need add segments at least one by the add_segment method. Returns: void + Raises: + NoSegmentError: If no segment are stored. + """ + if len(self._segments) < 1: + raise NoSegmentError() + + encoded_data = self._encode_data() self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() self._put_timing_pattern() self._put_version_information() - mask_area = self._put_data(data, encoder_class=encoder_class) + mask_area = self._put_data(encoded_data) self._apply_mask(mask_area) + def _encode_data(self): + qr_version = rMQRVersions[self.version_name()] + codewords_total = qr_version["codewords_total"] + + res = "" + for segment in self._segments: + character_count_indicator_length = qr_version["character_count_indicator_length"][segment["encoder_class"]] + res += segment["encoder_class"].encode(segment["data"], character_count_indicator_length) + res = self._append_terminator_if_needed(res, codewords_total) + + if len(res) > codewords_total * 8: + raise DataTooLongError("The data is too long.") + + return res + + def _append_terminator_if_needed(self, data, codewords_total): + if len(data) + 3 <= codewords_total * 8: + data += "000" + return data + def version_name(self): """Returns the version name. @@ -235,7 +265,6 @@ def to_list(self, with_quiet_zone=True): list: Converted list. """ - res = [] if with_quiet_zone: for y in range(self.QUIET_ZONE_MODULES): @@ -419,38 +448,31 @@ def _compute_version_info(self): version_information_data = version_information_data << 12 | reminder_polynomial return version_information_data - def _put_data(self, data, encoder_class=encoder.ByteEncoder): + def _put_data(self, encoded_data): """Symbol character placement. - This method puts data into the encoding region of the rMQR Code. Also this - method computes a two-dimensional list shows where encoding region at the + This method puts data into the encoding region of the rMQR Code. The data + should be encoded by NumericEncoder, AlphanumericEncoder, ByteEncoder or KanjiEncoder. + Also this method computes a two-dimensional list shows where encoding region at the same time. And returns the list. See: "7.7.3 Symbol character placement" in the ISO/IEC 23941. - Args: - data (str): Data string. - encoder_class (abc.ABCMeta): Pass a subclass of EncoderBase to select encoding mode. - Using ByteEncoder by default. - Returns: list: A two-dimensional list shows where encoding region. """ - qr_version = rMQRVersions[self.version_name()] - - character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] - codewords_total = qr_version["codewords_total"] - encoded_data = self._convert_to_bites_data( - data, character_count_indicator_length, codewords_total, encoder_class=encoder_class - ) + try: + encoded_data = self._encode_data() + except DataTooLongError: + raise DataTooLongError() + except NoSegmentError: + raise NoSegmentError() codewords = split_into_8bits(encoded_data) - print(codewords) - - if len(codewords) > codewords_total: - raise DataTooLongError("The data is too long.") # Add the remainder codewords + qr_version = rMQRVersions[self.version_name()] + codewords_total = qr_version["codewords_total"] while True: if len(codewords) >= codewords_total: break @@ -550,17 +572,6 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block - def _convert_to_bites_data( - self, data, character_count_indicator_length, codewords_total, encoder_class=encoder.ByteEncoder - ): - encoded_data = encoder_class.encode(data, character_count_indicator_length) - - # Terminator (may be truncated) - if len(encoded_data) + 3 <= codewords_total * 8: - encoded_data += "000" - - return encoded_data - def _apply_mask(self, mask_area): """Data masking. @@ -616,3 +627,8 @@ class DataTooLongError(ValueError): class IllegalVersionError(ValueError): "A class represents an error raised when the given version name is illegal." pass + + +class NoSegmentError(ValueError): + "A class represents an error raised when no segments are add" + pass diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index c075f77..8ea5500 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -2,6 +2,7 @@ from rmqrcode import ErrorCorrectionLevel from rmqrcode import DataTooLongError from rmqrcode import IllegalVersionError +from rmqrcode import NoSegmentError import pytest @@ -11,8 +12,9 @@ def test_fit(self): qr = rMQR.fit("abc") def test_make(self): - qr = rMQR('R13x99', ErrorCorrectionLevel.M) - qr.make("abc") + qr = rMQR("R13x99", ErrorCorrectionLevel.M) + qr.add_segment("abc") + qr.make() assert len(qr.to_list(with_quiet_zone=True)) is 17 assert len(qr.to_list(with_quiet_zone=True)[0]) is 103 @@ -20,6 +22,11 @@ def test_make(self): assert len(qr.to_list(with_quiet_zone=False)) is 13 assert len(qr.to_list(with_quiet_zone=False)[0]) is 99 + def test_raise_no_segment_error(self): + with pytest.raises(NoSegmentError) as e: + qr = rMQR("R13x99", ErrorCorrectionLevel.M) + qr.make() + def test_raise_too_long_error(self): with pytest.raises(DataTooLongError) as e: s = "a".ljust(200, "a") From 201e9418257e4fa31b272e6725493d0d10321d5c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 17:00:59 +0900 Subject: [PATCH 074/113] fix: Fix call rMQR#_encode_data twice --- src/rmqrcode/rmqrcode.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 5d57315..ae44068 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -157,8 +157,11 @@ def make(self): """ if len(self._segments) < 1: raise NoSegmentError() + try: + encoded_data = self._encode_data() + except DataTooLongError: + raise DataTooLongError() - encoded_data = self._encode_data() self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() @@ -455,19 +458,15 @@ def _put_data(self, encoded_data): should be encoded by NumericEncoder, AlphanumericEncoder, ByteEncoder or KanjiEncoder. Also this method computes a two-dimensional list shows where encoding region at the same time. And returns the list. - See: "7.7.3 Symbol character placement" in the ISO/IEC 23941. + Args: + encoded_data (str): The data after encoding. Expected all segments are joined. + Returns: list: A two-dimensional list shows where encoding region. """ - try: - encoded_data = self._encode_data() - except DataTooLongError: - raise DataTooLongError() - except NoSegmentError: - raise NoSegmentError() codewords = split_into_8bits(encoded_data) # Add the remainder codewords From 3a90f0308a19f77623cc4c72e5e2c5a70c20b4ee Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 18:06:22 +0900 Subject: [PATCH 075/113] doc: Add docstrings --- src/rmqrcode/rmqrcode.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index ae44068..6e23f02 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -140,6 +140,19 @@ def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._segments = [] def add_segment(self, data, encoder_class=encoder.ByteEncoder): + """Adds the segment. + + A segment consists of data and an encoding mode. + + Args: + data (str): The data. + encoder_class (abc.ABCMeta): Pass a subclass of EncoderBase to select encoding mode. + Using ByteEncoder by default. + + Returns: + void + + """ self._segments.append({"data": data, "encoder_class": encoder_class}) def make(self): @@ -171,6 +184,15 @@ def make(self): self._apply_mask(mask_area) def _encode_data(self): + """Encodes the data. + + This method encodes the data for added segments. This method concatenates the + encoded data of each segments. Finally, this concatenates the terminator if possible. + + Returns: + str: The encoded data. + + """ qr_version = rMQRVersions[self.version_name()] codewords_total = qr_version["codewords_total"] @@ -178,14 +200,28 @@ def _encode_data(self): for segment in self._segments: character_count_indicator_length = qr_version["character_count_indicator_length"][segment["encoder_class"]] res += segment["encoder_class"].encode(segment["data"], character_count_indicator_length) - res = self._append_terminator_if_needed(res, codewords_total) + res = self._append_terminator_if_possible(res, codewords_total) if len(res) > codewords_total * 8: raise DataTooLongError("The data is too long.") return res - def _append_terminator_if_needed(self, data, codewords_total): + def _append_terminator_if_possible(self, data, codewords_total): + """Appends the terminator. + + This method appends the terminator at the end of data and returns the + appended string. The terminator shall be omitted if the length of string + after appending the terminator greater than the rMQR code capacity. + + Args: + data: The data. + codewords_total: TODO: Fix + + Returns: + str: The string after appending the terminator. + + """ if len(data) + 3 <= codewords_total * 8: data += "000" return data From ec7850193c42efd3ec2f3c1fff242e023ffdaa3a Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 18:46:36 +0900 Subject: [PATCH 076/113] fix: Fix max length check --- src/rmqrcode/rmqrcode.py | 12 +++---- tests/rmqrcode_test.py | 67 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 6e23f02..71ee96f 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -194,20 +194,20 @@ def _encode_data(self): """ qr_version = rMQRVersions[self.version_name()] - codewords_total = qr_version["codewords_total"] + data_bits_max = DataCapacities[self.version_name()]["number_of_data_bits"][self._error_correction_level] res = "" for segment in self._segments: character_count_indicator_length = qr_version["character_count_indicator_length"][segment["encoder_class"]] res += segment["encoder_class"].encode(segment["data"], character_count_indicator_length) - res = self._append_terminator_if_possible(res, codewords_total) + res = self._append_terminator_if_possible(res, data_bits_max) - if len(res) > codewords_total * 8: + if len(res) > data_bits_max: raise DataTooLongError("The data is too long.") return res - def _append_terminator_if_possible(self, data, codewords_total): + def _append_terminator_if_possible(self, data, data_bits_max): """Appends the terminator. This method appends the terminator at the end of data and returns the @@ -216,13 +216,13 @@ def _append_terminator_if_possible(self, data, codewords_total): Args: data: The data. - codewords_total: TODO: Fix + data_bits_max: The max length of data bits. Returns: str: The string after appending the terminator. """ - if len(data) + 3 <= codewords_total * 8: + if len(data) + 3 <= data_bits_max: data += "000" return data diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index 8ea5500..911bf94 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -1,8 +1,11 @@ -from rmqrcode import rMQR -from rmqrcode import ErrorCorrectionLevel -from rmqrcode import DataTooLongError -from rmqrcode import IllegalVersionError -from rmqrcode import NoSegmentError +from rmqrcode import ( + rMQR, + encoder, + ErrorCorrectionLevel, + DataTooLongError, + IllegalVersionError, + NoSegmentError, +) import pytest @@ -27,7 +30,59 @@ def test_raise_no_segment_error(self): qr = rMQR("R13x99", ErrorCorrectionLevel.M) qr.make() - def test_raise_too_long_error(self): + def test_can_make_max_length_numeric_encoder(self): + s = "1" * 361 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.NumericEncoder) + qr.make() + + def test_raise_too_long_error_numeric_encoder(self): + with pytest.raises(DataTooLongError) as e: + s = "1" * 362 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.NumericEncoder) + qr.make() + + def test_can_make_max_length_alphanumeric_encoder(self): + s = "A" * 219 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.AlphanumericEncoder) + qr.make() + + def test_raise_too_long_error_alphanumeric_encoder(self): + with pytest.raises(DataTooLongError) as e: + s = "A" * 220 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.AlphanumericEncoder) + qr.make() + + def test_can_make_max_length_byte_encoder(self): + s = "a" * 150 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.ByteEncoder) + qr.make() + + def test_raise_too_long_error_byte_encoder(self): + with pytest.raises(DataTooLongError) as e: + s = "a" * 151 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.ByteEncoder) + qr.make() + + def test_can_make_max_length_kanji_encoder(self): + s = "漢" * 92 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.KanjiEncoder) + qr.make() + + def test_raise_too_long_error_kanji_encoder(self): + with pytest.raises(DataTooLongError) as e: + s = "漢" * 93 + qr = rMQR("R17x139", ErrorCorrectionLevel.M) + qr.add_segment(s, encoder_class=encoder.KanjiEncoder) + qr.make() + + def test_raise_too_long_error_fit(self): with pytest.raises(DataTooLongError) as e: s = "a".ljust(200, "a") rMQR.fit(s) From 414bb480432a7fa44689f2ab4f584b69066a3801 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 19:31:56 +0900 Subject: [PATCH 077/113] doc: Update README.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index de6605e..d1427c4 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ image.save("my_qr.png") ### Select rMQR Code size manually To select rMQR Code size manually, use `rMQR()` constructor. ```py +from rmqrcode import rMQR, ErrorCorrectionLevel, encoder qr = rMQR('R11x139', ErrorCorrectionLevel.H) -qr.make("https://oudon.xyz") ``` `R11x139` means 11 rows and 139 columns. The following table shows available combinations. @@ -103,14 +103,18 @@ qr.make("https://oudon.xyz") |R15|❌|✅|✅|✅|✅|✅| |R17|❌|✅|✅|✅|✅|✅| -### Encoding modes +### Encoding Modes and Segments -The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. The following example shows how to encode data "123456" in the Numeric mode. We can select an encoding mode by passing the `encoder_class` argument to the `rMQR#make` method. In this case, the length of bits after encoding is 27 in the Numeric mode, which is shorter than 56 in the Byte mode. +The rMQR Code has the four encoding modes Numeric, Alphanumeric, Byte and Kanji to convert data efficiently. We can select encoding mode for each data segment separately. +The following example shows how to encode data "123Abc". The first segment is for "123" in the Numeric mode. The second segment is for "Abc" in the Byte mode. +We can select an encoding mode by passing the `encoder_class` argument to the `rMQR#add_segment` method. In this example, the length of bits after encoding is 47 in the case combined with the Numeric mode and the Byte mode, which is shorter than 56 in the Byte mode only. ```py from rmqrcode import rMQR, ErrorCorrectionLevel, encoder -qr = rMQR('R13x43', ErrorCorrectionLevel.M) -qr.make("123456", encoder_class=encoder.NumericEncoder) +qr = rMQR('R7x43', ErrorCorrectionLevel.M) +qr.add_segment("123", encoder_class=encoder.NumericEncoder) +qr.add_segment("Abc", encoder_class=encoder.ByteEncoder) +qr.make() ``` The value for `encoder_class` is listed in the below table. @@ -118,12 +122,10 @@ The value for `encoder_class` is listed in the below table. |Mode|Value of encoder_class|Characters| |-|-|-| |Numeric|NumericEncoder|0-9| -|Alphanumeric|AlphanumericEncoder|0-9 A-Z \s $ % * + - . / :| +|Alphanumeric|AlphanumericEncoder|0-9 A-Z \s $ % * + - . / :| |Byte|ByteEncoder|Any| |Kanji|KanjiEncoder|from 0x8140 to 0x9FFC, from 0xE040 to 0xEBBF in Shift JIS value| -The rMQR Code also supports mixed modes in order to encode more efficiently. However, this package has not supported this feature yet. - ## 🤝 Contributing Any suggestions are welcome! If you are interesting in contributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). From 98ee427dad382fb5bff98c8e4d622038999a0e19 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 18 Aug 2022 19:33:30 +0900 Subject: [PATCH 078/113] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1427c4..4b67869 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ image.save("my_qr.png") ### Select rMQR Code size manually To select rMQR Code size manually, use `rMQR()` constructor. ```py -from rmqrcode import rMQR, ErrorCorrectionLevel, encoder +from rmqrcode import rMQR, ErrorCorrectionLevel qr = rMQR('R11x139', ErrorCorrectionLevel.H) ``` From 4b50b2e88612074f4baac5f0bfcf37039ee8dba1 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 19 Aug 2022 07:13:54 +0900 Subject: [PATCH 079/113] feat: Update example.py --- example.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 35a9f84..983c57d 100644 --- a/example.py +++ b/example.py @@ -27,8 +27,9 @@ def main(): # Determine rMQR version manually # version = 'R7x43' # qr = rMQR(version, error_correction_level) - # Also you can select encoding mode manually - # qr.make("123456", encoder_class=encoder.NumericEncoder) + # qr.add_segment("123", encoder_class=encoder.NumericEncoder) + # qr.add_segment("Abc", encoder_class=encoder.ByteEncoder) + # qr.make() # print(qr) # Save as png From 0210a6df566c16be5486cc8b60c9089dc82269c7 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 19 Aug 2022 09:40:31 +0900 Subject: [PATCH 080/113] feat: Implement optimize segmentation algorithm --- src/rmqrcode/rmqrcode.py | 118 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 71ee96f..82ee386 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -125,6 +125,124 @@ def sort_key(x): qr.make() return qr + def _compute_optimize_segmentation(self, data): + qr_version = rMQRVersions[self.version_name()] + INF = 1000 + + dp = [[[INF for n in range(3)] for mode in range(4)] for length in range(1300)] + parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(1300)] + + encoders = [ + encoder.NumericEncoder, + encoder.AlphanumericEncoder, + encoder.ByteEncoder, + encoder.KanjiEncoder, + ] + + # Set initial costs + for mode in range(len(encoders)): + encoder_class = encoders[mode] + character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] + dp[0][mode][0] = encoder_class.length("", character_count_indicator_length) + parents[0][mode][0] = (0, 0) + + # Compute all costs + for n in range(0, len(data)): + print("----") + print(f"{n} -> {n+1}") + print(dp[n]) + for mode in range(4): + for length in range(3): + if dp[n][mode][length] == INF: + continue + + for new_mode in range(4): + if not encoders[new_mode].is_valid_characters(data[n]): + continue + + encoder_class = encoders[new_mode] + character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] + if new_mode == mode: + # Keep the mode + if encoder_class == encoder.NumericEncoder: + new_length = (length + 1) % 3 + cost = 4 if length == 0 else 3 + elif encoder_class == encoder.AlphanumericEncoder: + new_length = (length + 1) % 2 + cost = 6 if length == 0 else 5 + elif encoder_class == encoder.ByteEncoder: + new_length = 0 + cost = 8 + elif encoder_class == encoder.KanjiEncoder: + new_length = 0 + cost = 13 + else: + # Change the mode + if encoder_class == encoder.NumericEncoder: + new_length = 1 + cost = encoders[new_mode].length(data[n], character_count_indicator_length) + elif encoder_class == encoder.AlphanumericEncoder: + new_length = 1 + cost = encoders[new_mode].length(data[n], character_count_indicator_length) + elif encoder_class == encoder.ByteEncoder: + new_length = 0 + cost = encoders[new_mode].length(data[n], character_count_indicator_length) + elif encoder_class == encoder.KanjiEncoder: + new_length = 0 + cost = encoders[new_mode].length(data[n], character_count_indicator_length) + + if dp[n][mode][length] + cost < dp[n+1][new_mode][new_length]: + dp[n+1][new_mode][new_length] = dp[n][mode][length] + cost + parents[n+1][new_mode][new_length] = (n, mode, length) + + print("=======") + print(dp[len(data)]) + + # Find the best + best = INF + best_index = (-1, -1) + for mode in range(4): + for length in range(3): + if dp[len(data)][mode][length] < best: + best = dp[len(data)][mode][length] + best_index = (len(data), mode, length) + + # Reconstruct the path + path = [] + index = best_index + while index != (0, 0): + path.append(index) + index = parents[index[0]][index[1]][index[2]] + path.reverse() + path = path[1:] + print(path) + + # Compute the segments + segments = [] + current_segment_data = "" + current_mode = -1 + for p in path: + if current_mode == -1: + current_mode = p[1] + current_segment_data += data[p[0] - 1] + elif current_mode == p[1]: + current_segment_data += data[p[0] - 1] + else: + segments.append({ + "data": current_segment_data, + "encoder_class": encoders[current_mode] + }) + current_segment_data = data[p[0] - 1] + current_mode = p[1] + if current_mode != -1: + segments.append({ + "data": current_segment_data, + "encoder_class": encoders[current_mode] + }) + print(segments) + + return segments + def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() From 118c6bfb5cd518caec121c8a0b91a52d85912537 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 19 Aug 2022 09:45:52 +0900 Subject: [PATCH 081/113] fix: Fix incorrect indent --- src/rmqrcode/rmqrcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 82ee386..df508de 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -241,7 +241,7 @@ def _compute_optimize_segmentation(self, data): }) print(segments) - return segments + return segments def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() From 8091e0ac9a1b294b3157d50a5e7aed74a7bd87f2 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 22 Aug 2022 15:50:17 +0900 Subject: [PATCH 082/113] feat: Pre-check character limit in _compute_optimize_segmentation() --- src/rmqrcode/rmqrcode.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index df508de..8d45e26 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -126,11 +126,14 @@ def sort_key(x): return qr def _compute_optimize_segmentation(self, data): - qr_version = rMQRVersions[self.version_name()] INF = 1000 + MAX_CHARACTER = 360 + if len(data) > MAX_CHARACTER: + raise DataTooLongError() - dp = [[[INF for n in range(3)] for mode in range(4)] for length in range(1300)] - parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(1300)] + qr_version = rMQRVersions[self.version_name()] + dp = [[[INF for n in range(3)] for mode in range(4)] for length in range(MAX_CHARACTER + 1)] + parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(MAX_CHARACTER + 1)] encoders = [ encoder.NumericEncoder, @@ -178,18 +181,11 @@ def _compute_optimize_segmentation(self, data): cost = 13 else: # Change the mode - if encoder_class == encoder.NumericEncoder: + if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: new_length = 1 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) - elif encoder_class == encoder.AlphanumericEncoder: - new_length = 1 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) - elif encoder_class == encoder.ByteEncoder: - new_length = 0 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) - elif encoder_class == encoder.KanjiEncoder: + elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: new_length = 0 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) + cost = encoders[new_mode].length(data[n], character_count_indicator_length) if dp[n][mode][length] + cost < dp[n+1][new_mode][new_length]: dp[n+1][new_mode][new_length] = dp[n][mode][length] + cost @@ -239,7 +235,6 @@ def _compute_optimize_segmentation(self, data): "data": current_segment_data, "encoder_class": encoders[current_mode] }) - print(segments) return segments From 2ff35ed31139da77d06f5ec8614c40b04750788a Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 22 Aug 2022 18:15:22 +0900 Subject: [PATCH 083/113] refactor: Move the implementation of compute_optimize_segmentation to SegmentsOptimizer --- src/rmqrcode/errors.py | 13 ++++ src/rmqrcode/rmqrcode.py | 131 ++------------------------------------- src/rmqrcode/segments.py | 129 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 126 deletions(-) create mode 100644 src/rmqrcode/errors.py create mode 100644 src/rmqrcode/segments.py diff --git a/src/rmqrcode/errors.py b/src/rmqrcode/errors.py new file mode 100644 index 0000000..c8a9769 --- /dev/null +++ b/src/rmqrcode/errors.py @@ -0,0 +1,13 @@ +class DataTooLongError(ValueError): + "A class represents an error raised when the given data is too long." + pass + + +class IllegalVersionError(ValueError): + "A class represents an error raised when the given version name is illegal." + pass + + +class NoSegmentError(ValueError): + "A class represents an error raised when no segments are add" + pass diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 8d45e26..e869cd5 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -29,6 +29,8 @@ from .format.rmqr_versions import rMQRVersions from .util.error_correction import compute_bch, compute_reed_solomon from .util.utilities import split_into_8bits +from .segments import SegmentOptimizer +from .errors import DataTooLongError, IllegalVersionError, NoSegmentError class rMQR: @@ -125,117 +127,9 @@ def sort_key(x): qr.make() return qr - def _compute_optimize_segmentation(self, data): - INF = 1000 - MAX_CHARACTER = 360 - if len(data) > MAX_CHARACTER: - raise DataTooLongError() - - qr_version = rMQRVersions[self.version_name()] - dp = [[[INF for n in range(3)] for mode in range(4)] for length in range(MAX_CHARACTER + 1)] - parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(MAX_CHARACTER + 1)] - - encoders = [ - encoder.NumericEncoder, - encoder.AlphanumericEncoder, - encoder.ByteEncoder, - encoder.KanjiEncoder, - ] - - # Set initial costs - for mode in range(len(encoders)): - encoder_class = encoders[mode] - character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] - dp[0][mode][0] = encoder_class.length("", character_count_indicator_length) - parents[0][mode][0] = (0, 0) - - # Compute all costs - for n in range(0, len(data)): - print("----") - print(f"{n} -> {n+1}") - print(dp[n]) - for mode in range(4): - for length in range(3): - if dp[n][mode][length] == INF: - continue - - for new_mode in range(4): - if not encoders[new_mode].is_valid_characters(data[n]): - continue - - encoder_class = encoders[new_mode] - character_count_indicator_length = qr_version["character_count_indicator_length"][encoder_class] - if new_mode == mode: - # Keep the mode - if encoder_class == encoder.NumericEncoder: - new_length = (length + 1) % 3 - cost = 4 if length == 0 else 3 - elif encoder_class == encoder.AlphanumericEncoder: - new_length = (length + 1) % 2 - cost = 6 if length == 0 else 5 - elif encoder_class == encoder.ByteEncoder: - new_length = 0 - cost = 8 - elif encoder_class == encoder.KanjiEncoder: - new_length = 0 - cost = 13 - else: - # Change the mode - if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: - new_length = 1 - elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: - new_length = 0 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) - - if dp[n][mode][length] + cost < dp[n+1][new_mode][new_length]: - dp[n+1][new_mode][new_length] = dp[n][mode][length] + cost - parents[n+1][new_mode][new_length] = (n, mode, length) - - print("=======") - print(dp[len(data)]) - - # Find the best - best = INF - best_index = (-1, -1) - for mode in range(4): - for length in range(3): - if dp[len(data)][mode][length] < best: - best = dp[len(data)][mode][length] - best_index = (len(data), mode, length) - - # Reconstruct the path - path = [] - index = best_index - while index != (0, 0): - path.append(index) - index = parents[index[0]][index[1]][index[2]] - path.reverse() - path = path[1:] - print(path) - - # Compute the segments - segments = [] - current_segment_data = "" - current_mode = -1 - for p in path: - if current_mode == -1: - current_mode = p[1] - current_segment_data += data[p[0] - 1] - elif current_mode == p[1]: - current_segment_data += data[p[0] - 1] - else: - segments.append({ - "data": current_segment_data, - "encoder_class": encoders[current_mode] - }) - current_segment_data = data[p[0] - 1] - current_mode = p[1] - if current_mode != -1: - segments.append({ - "data": current_segment_data, - "encoder_class": encoders[current_mode] - }) - + def _optimized_segments(self, data): + optimizer = SegmentOptimizer() + segments = optimizer.compute(data, self.version_name()) return segments def __init__(self, version, ecc, with_quiet_zone=True, logger=None): @@ -765,18 +659,3 @@ def validate_version(version_name): """ return version_name in rMQRVersions - - -class DataTooLongError(ValueError): - "A class represents an error raised when the given data is too long." - pass - - -class IllegalVersionError(ValueError): - "A class represents an error raised when the given version name is illegal." - pass - - -class NoSegmentError(ValueError): - "A class represents an error raised when no segments are add" - pass diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py new file mode 100644 index 0000000..a1698ae --- /dev/null +++ b/src/rmqrcode/segments.py @@ -0,0 +1,129 @@ +from .format.rmqr_versions import rMQRVersions +from .errors import DataTooLongError +from . import encoder + +encoders = [ + encoder.NumericEncoder, + encoder.AlphanumericEncoder, + encoder.ByteEncoder, + encoder.KanjiEncoder, +] + +class SegmentOptimizer: + MAX_CHARACTER = 360 + INF = 1000 + + def __init__(self): + self.dp = [[[self.INF for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] + self.parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] + + def compute(self, data, version): + if len(data) > self.MAX_CHARACTER: + raise DataTooLongError() + + self.qr_version = rMQRVersions[version] + self._compute_costs(data) + best_index = self._find_best(data) + path = self._reconstruct_path(best_index) + segments = self._compute_segments(path, data) + return segments + + def _compute_costs(self, data): + """Compute costs""" + for mode in range(len(encoders)): + encoder_class = encoders[mode] + character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] + self.dp[0][mode][0] = encoder_class.length("", character_count_indicator_length) + self.parents[0][mode][0] = (0, 0) + + for n in range(0, len(data)): + print("----") + print(f"{n} -> {n+1}") + print(self.dp[n]) + for mode in range(4): + for length in range(3): + if self.dp[n][mode][length] == self.INF: + continue + + for new_mode in range(4): + if not encoders[new_mode].is_valid_characters(data[n]): + continue + + encoder_class = encoders[new_mode] + character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] + if new_mode == mode: + # Keep the mode + if encoder_class == encoder.NumericEncoder: + new_length = (length + 1) % 3 + cost = 4 if length == 0 else 3 + elif encoder_class == encoder.AlphanumericEncoder: + new_length = (length + 1) % 2 + cost = 6 if length == 0 else 5 + elif encoder_class == encoder.ByteEncoder: + new_length = 0 + cost = 8 + elif encoder_class == encoder.KanjiEncoder: + new_length = 0 + cost = 13 + else: + # Change the mode + if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: + new_length = 1 + elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: + new_length = 0 + cost = encoders[new_mode].length(data[n], character_count_indicator_length) + + if self.dp[n][mode][length] + cost < self.dp[n+1][new_mode][new_length]: + self.dp[n+1][new_mode][new_length] = self.dp[n][mode][length] + cost + self.parents[n+1][new_mode][new_length] = (n, mode, length) + + print("=======") + print(self.dp[len(data)]) + + def _find_best(self, data): + """Find the best""" + best = self.INF + best_index = (-1, -1) + for mode in range(4): + for length in range(3): + if self.dp[len(data)][mode][length] < best: + best = self.dp[len(data)][mode][length] + best_index = (len(data), mode, length) + return best_index + + def _reconstruct_path(self, best_index): + """Reconstruct the path""" + path = [] + index = best_index + while index != (0, 0): + path.append(index) + index = self.parents[index[0]][index[1]][index[2]] + path.reverse() + path = path[1:] + print(path) + return path + + def _compute_segments(self, path, data): + """Compute the segments""" + segments = [] + current_segment_data = "" + current_mode = -1 + for p in path: + if current_mode == -1: + current_mode = p[1] + current_segment_data += data[p[0] - 1] + elif current_mode == p[1]: + current_segment_data += data[p[0] - 1] + else: + segments.append({ + "data": current_segment_data, + "encoder_class": encoders[current_mode] + }) + current_segment_data = data[p[0] - 1] + current_mode = p[1] + if current_mode != -1: + segments.append({ + "data": current_segment_data, + "encoder_class": encoders[current_mode] + }) + return segments From 087ee2886a643f31fbc5e46abbaf900ce3072f9c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 22 Aug 2022 18:16:28 +0900 Subject: [PATCH 084/113] chore: Apply formatter --- src/rmqrcode/rmqrcode.py | 4 ++-- src/rmqrcode/segments.py | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index e869cd5..ec800e0 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -21,16 +21,16 @@ from . import encoder from .enums.color import Color from .enums.fit_strategy import FitStrategy +from .errors import DataTooLongError, IllegalVersionError, NoSegmentError from .format.alignment_pattern_coordinates import AlignmentPatternCoordinates from .format.data_capacities import DataCapacities from .format.error_correction_level import ErrorCorrectionLevel from .format.generator_polynomials import GeneratorPolynomials from .format.mask import mask from .format.rmqr_versions import rMQRVersions +from .segments import SegmentOptimizer from .util.error_correction import compute_bch, compute_reed_solomon from .util.utilities import split_into_8bits -from .segments import SegmentOptimizer -from .errors import DataTooLongError, IllegalVersionError, NoSegmentError class rMQR: diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index a1698ae..d6ec714 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -1,6 +1,6 @@ -from .format.rmqr_versions import rMQRVersions -from .errors import DataTooLongError from . import encoder +from .errors import DataTooLongError +from .format.rmqr_versions import rMQRVersions encoders = [ encoder.NumericEncoder, @@ -9,6 +9,7 @@ encoder.KanjiEncoder, ] + class SegmentOptimizer: MAX_CHARACTER = 360 INF = 1000 @@ -50,7 +51,9 @@ def _compute_costs(self, data): continue encoder_class = encoders[new_mode] - character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] + character_count_indicator_length = self.qr_version["character_count_indicator_length"][ + encoder_class + ] if new_mode == mode: # Keep the mode if encoder_class == encoder.NumericEncoder: @@ -73,9 +76,9 @@ def _compute_costs(self, data): new_length = 0 cost = encoders[new_mode].length(data[n], character_count_indicator_length) - if self.dp[n][mode][length] + cost < self.dp[n+1][new_mode][new_length]: - self.dp[n+1][new_mode][new_length] = self.dp[n][mode][length] + cost - self.parents[n+1][new_mode][new_length] = (n, mode, length) + if self.dp[n][mode][length] + cost < self.dp[n + 1][new_mode][new_length]: + self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][length] + cost + self.parents[n + 1][new_mode][new_length] = (n, mode, length) print("=======") print(self.dp[len(data)]) @@ -115,15 +118,9 @@ def _compute_segments(self, path, data): elif current_mode == p[1]: current_segment_data += data[p[0] - 1] else: - segments.append({ - "data": current_segment_data, - "encoder_class": encoders[current_mode] - }) + segments.append({"data": current_segment_data, "encoder_class": encoders[current_mode]}) current_segment_data = data[p[0] - 1] current_mode = p[1] if current_mode != -1: - segments.append({ - "data": current_segment_data, - "encoder_class": encoders[current_mode] - }) + segments.append({"data": current_segment_data, "encoder_class": encoders[current_mode]}) return segments From b5f0c23bc7a50e7e12c4ff4f8c43cc2ea7fbfe6c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 07:19:03 +0900 Subject: [PATCH 085/113] doc: Add docstrings to SegmentOptimizer --- src/rmqrcode/segments.py | 48 +++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index d6ec714..7ee259f 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -19,6 +19,16 @@ def __init__(self): self.parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] def compute(self, data, version): + """Computes the optimize segmentation for the given data. + + Args: + data (str): The data to encode. + version (str): The version name. + + Returns: + list: The list of segments. + + """ if len(data) > self.MAX_CHARACTER: raise DataTooLongError() @@ -30,12 +40,24 @@ def compute(self, data, version): return segments def _compute_costs(self, data): - """Compute costs""" + """Computes costs by dynamic programming. + + This method computes costs of the dynamic programming table. Define dp[n][mode][length] as + the minimize bit length when encode only the `n`-th leading characters which the last + character is encoded in `mode` and the remainder bits length is `length`. + + Args: + data (str): The data to encode + + Returns: + void + + """ for mode in range(len(encoders)): encoder_class = encoders[mode] character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] self.dp[0][mode][0] = encoder_class.length("", character_count_indicator_length) - self.parents[0][mode][0] = (0, 0) + self.parents[0][mode][0] = (0, 0, 0) for n in range(0, len(data)): print("----") @@ -84,7 +106,15 @@ def _compute_costs(self, data): print(self.dp[len(data)]) def _find_best(self, data): - """Find the best""" + """Find the index which has the minimum costs. + + Args: + data (str): The data to encode + + Returns: + tuple: The best index as tuple (n, mode, length). + + """ best = self.INF best_index = (-1, -1) for mode in range(4): @@ -95,10 +125,18 @@ def _find_best(self, data): return best_index def _reconstruct_path(self, best_index): - """Reconstruct the path""" + """Reconstruct the path + + Args: + best_index + + Returns: + list: The path of minimum cost in the dynamic programming table + + """ path = [] index = best_index - while index != (0, 0): + while index != (0, 0, 0): path.append(index) index = self.parents[index[0]][index[1]][index[2]] path.reverse() From 134ae672516deb7737b38c28f2cbd7d34afc8fd5 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 07:21:04 +0900 Subject: [PATCH 086/113] doc: Update docstrings --- src/rmqrcode/segments.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 7ee259f..5298ffb 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -125,10 +125,10 @@ def _find_best(self, data): return best_index def _reconstruct_path(self, best_index): - """Reconstruct the path + """Reconstructs the path. Args: - best_index + best_index: The best index computed by self._find_best(). Returns: list: The path of minimum cost in the dynamic programming table @@ -145,7 +145,18 @@ def _reconstruct_path(self, best_index): return path def _compute_segments(self, path, data): - """Compute the segments""" + """Computes the segments + + This method computes the segments. The adjacent characters has same mode are merged. + + Args: + path (list): The path computed by self._reconstruct_path(). + data (str): The data to encode. + + Returns: + list: The list of segments. + + """ segments = [] current_segment_data = "" current_mode = -1 From 8fde26dee8fc44e9f82094f361b6d64759483742 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 08:13:16 +0900 Subject: [PATCH 087/113] feat: Use SegmentOptimizer in rMQR.fit --- src/rmqrcode/rmqrcode.py | 14 ++++++++++---- src/rmqrcode/segments.py | 14 ++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index ec800e0..82c8b6c 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -84,9 +84,10 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - data_length = encoder_class.length( - data, rMQRVersions[version_name]["character_count_indicator_length"][encoder_class] - ) + optimizer = SegmentOptimizer() + segments = optimizer.compute(data, version_name) + data_length = sum(map(lambda s:s["encoder_class"].length(s["data"], rMQRVersions[version_name]["character_count_indicator_length"][s["encoder_class"]]), segments)) + if data_length <= qr_version["number_of_data_bits"][ecc]: width, height = qr_version["width"], qr_version["height"] if width not in determined_width and height not in determined_height: @@ -97,6 +98,7 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): "version": version_name, "width": width, "height": height, + "segments": segments, } ) logger.debug(f"ok: {version_name}") @@ -123,7 +125,7 @@ def sort_key(x): logger.debug(f"selected: {selected}") qr = rMQR(selected["version"], ecc) - qr.add_segment(data, encoder_class) + qr.add_segments(selected["segments"]) qr.make() return qr @@ -162,6 +164,10 @@ def add_segment(self, data, encoder_class=encoder.ByteEncoder): """ self._segments.append({"data": data, "encoder_class": encoder_class}) + def add_segments(self, segments): + for segment in segments: + self.add_segment(segment["data"], segment["encoder_class"]) + def make(self): """Makes an rMQR Code for stored segments. diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 5298ffb..51fe19f 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -60,9 +60,9 @@ def _compute_costs(self, data): self.parents[0][mode][0] = (0, 0, 0) for n in range(0, len(data)): - print("----") - print(f"{n} -> {n+1}") - print(self.dp[n]) + # print("----") + # print(f"{n} -> {n+1}") + # print(self.dp[n]) for mode in range(4): for length in range(3): if self.dp[n][mode][length] == self.INF: @@ -102,8 +102,8 @@ def _compute_costs(self, data): self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][length] + cost self.parents[n + 1][new_mode][new_length] = (n, mode, length) - print("=======") - print(self.dp[len(data)]) + # print("=======") + # print(self.dp[len(data)]) def _find_best(self, data): """Find the index which has the minimum costs. @@ -140,12 +140,10 @@ def _reconstruct_path(self, best_index): path.append(index) index = self.parents[index[0]][index[1]][index[2]] path.reverse() - path = path[1:] - print(path) return path def _compute_segments(self, path, data): - """Computes the segments + """Computes the segments. This method computes the segments. The adjacent characters has same mode are merged. From dbe93aca0d4c6808575e15cd53fbc9487b19e553 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 08:14:10 +0900 Subject: [PATCH 088/113] chore: Apply formatter and linter --- src/rmqrcode/rmqrcode.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 82c8b6c..f928605 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -79,14 +79,18 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): determined_width = set() determined_height = set() - # Fixed value currently - encoder_class = encoder.ByteEncoder - logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): optimizer = SegmentOptimizer() segments = optimizer.compute(data, version_name) - data_length = sum(map(lambda s:s["encoder_class"].length(s["data"], rMQRVersions[version_name]["character_count_indicator_length"][s["encoder_class"]]), segments)) + data_length = sum( + map( + lambda s: s["encoder_class"].length( + s["data"], rMQRVersions[version_name]["character_count_indicator_length"][s["encoder_class"]] + ), + segments, + ) + ) if data_length <= qr_version["number_of_data_bits"][ecc]: width, height = qr_version["width"], qr_version["height"] From f3bd9a45e59746698a54f6a08b70a5c552a5d86d Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 08:31:54 +0900 Subject: [PATCH 089/113] fix: Fix the issue caused INF is too small --- src/rmqrcode/segments.py | 4 ++-- tests/rmqrcode_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 51fe19f..8a33125 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -12,7 +12,7 @@ class SegmentOptimizer: MAX_CHARACTER = 360 - INF = 1000 + INF = 100000 def __init__(self): self.dp = [[[self.INF for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] @@ -136,7 +136,7 @@ def _reconstruct_path(self, best_index): """ path = [] index = best_index - while index != (0, 0, 0): + while index[0] != 0: path.append(index) index = self.parents[index[0]][index[1]][index[2]] path.reverse() diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index 911bf94..ab64012 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -84,7 +84,7 @@ def test_raise_too_long_error_kanji_encoder(self): def test_raise_too_long_error_fit(self): with pytest.raises(DataTooLongError) as e: - s = "a".ljust(200, "a") + s = "a" * 200 rMQR.fit(s) def test_raise_invalid_version_error(self): From 4e7c94bf11543684c3907034907766f8b29c736e Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 09:12:48 +0900 Subject: [PATCH 090/113] refactor: Move implementaion compute the sum of length of segments to segments.compute_length() --- src/rmqrcode/rmqrcode.py | 20 ++++++-------------- src/rmqrcode/segments.py | 29 +++++++++++++++++++++++++++++ tests/rmqrcode_test.py | 3 +-- tests/segments_test.py | 18 ++++++++++++++++++ 4 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 tests/segments_test.py diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index f928605..a3519a2 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -19,6 +19,7 @@ import logging from . import encoder +from . import segments as qr_segments from .enums.color import Color from .enums.fit_strategy import FitStrategy from .errors import DataTooLongError, IllegalVersionError, NoSegmentError @@ -28,7 +29,6 @@ from .format.generator_polynomials import GeneratorPolynomials from .format.mask import mask from .format.rmqr_versions import rMQRVersions -from .segments import SegmentOptimizer from .util.error_correction import compute_bch, compute_reed_solomon from .util.utilities import split_into_8bits @@ -81,16 +81,9 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): - optimizer = SegmentOptimizer() - segments = optimizer.compute(data, version_name) - data_length = sum( - map( - lambda s: s["encoder_class"].length( - s["data"], rMQRVersions[version_name]["character_count_indicator_length"][s["encoder_class"]] - ), - segments, - ) - ) + optimizer = qr_segments.SegmentOptimizer() + optimized_segments = optimizer.compute(data, version_name) + data_length = qr_segments.compute_length(optimized_segments, version_name) if data_length <= qr_version["number_of_data_bits"][ecc]: width, height = qr_version["width"], qr_version["height"] @@ -102,7 +95,7 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): "version": version_name, "width": width, "height": height, - "segments": segments, + "segments": optimized_segments, } ) logger.debug(f"ok: {version_name}") @@ -135,8 +128,7 @@ def sort_key(x): def _optimized_segments(self, data): optimizer = SegmentOptimizer() - segments = optimizer.compute(data, self.version_name()) - return segments + return optimizer.compute(data, self.version_name()) def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 8a33125..163925b 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -10,7 +10,36 @@ ] +def compute_length(segments, version_name): + """Computes the sum of length of the segments. + + Args: + segments (list): The list of segment. + version_name (str): The version name. + + Returns: + int: The sum of the length of the segments. + + """ + return sum( + map( + lambda s: s["encoder_class"].length( + s["data"], rMQRVersions[version_name]["character_count_indicator_length"][s["encoder_class"]] + ), + segments, + ) + ) + + class SegmentOptimizer: + """A class for computing optimal segmentation of the given data by dynamic programming. + + Attributes: + MAX_CHARACTER (int): The maximum characters of the given data. + INF (int): Large enough value. This is used as initial value of the dynamic programming table. + + """ + MAX_CHARACTER = 360 INF = 100000 diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index ab64012..58d4361 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -84,8 +84,7 @@ def test_raise_too_long_error_kanji_encoder(self): def test_raise_too_long_error_fit(self): with pytest.raises(DataTooLongError) as e: - s = "a" * 200 - rMQR.fit(s) + rMQR.fit("a" * 200) def test_raise_invalid_version_error(self): with pytest.raises(IllegalVersionError) as e: diff --git a/tests/segments_test.py b/tests/segments_test.py new file mode 100644 index 0000000..073d230 --- /dev/null +++ b/tests/segments_test.py @@ -0,0 +1,18 @@ +from rmqrcode.segments import SegmentOptimizer, compute_length +from rmqrcode import encoder +import pytest + + +class TestSegments: + def test_can_optimize_segments(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123Abc", "R7x43") + assert segments == [ + {"data": "123", "encoder_class": encoder.NumericEncoder}, + {"data": "Abc", "encoder_class": encoder.ByteEncoder}, + ] + + def test_compute_length(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123Abc", "R7x43") + assert compute_length(segments, "R7x43") is 47 From c4fc973fa391b345892205ccb6e30a8deba2ca6b Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 09:14:12 +0900 Subject: [PATCH 091/113] fix: Fix missing prefix --- src/rmqrcode/rmqrcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index a3519a2..74dc670 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -127,7 +127,7 @@ def sort_key(x): return qr def _optimized_segments(self, data): - optimizer = SegmentOptimizer() + optimizer = qr_segments.SegmentOptimizer() return optimizer.compute(data, self.version_name()) def __init__(self, version, ecc, with_quiet_zone=True, logger=None): From de32439cd0352b41abc7704b3c83fd7fed80b9d1 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 10:24:35 +0900 Subject: [PATCH 092/113] chore: Rename length to unfilled_length in segments --- src/rmqrcode/segments.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 163925b..2d8bbcb 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -71,9 +71,10 @@ def compute(self, data, version): def _compute_costs(self, data): """Computes costs by dynamic programming. - This method computes costs of the dynamic programming table. Define dp[n][mode][length] as - the minimize bit length when encode only the `n`-th leading characters which the last - character is encoded in `mode` and the remainder bits length is `length`. + This method computes costs of the dynamic programming table. Define + dp[n][mode][unfilled_length] as the minimize bit length when encode only + the `n`-th leading characters which the last character is encoded in `mode` + and the remainder bits length is `unfilled_length`. Args: data (str): The data to encode @@ -93,8 +94,8 @@ def _compute_costs(self, data): # print(f"{n} -> {n+1}") # print(self.dp[n]) for mode in range(4): - for length in range(3): - if self.dp[n][mode][length] == self.INF: + for unfilled_length in range(3): + if self.dp[n][mode][unfilled_length] == self.INF: continue for new_mode in range(4): @@ -108,11 +109,11 @@ def _compute_costs(self, data): if new_mode == mode: # Keep the mode if encoder_class == encoder.NumericEncoder: - new_length = (length + 1) % 3 - cost = 4 if length == 0 else 3 + new_length = (unfilled_length + 1) % 3 + cost = 4 if unfilled_length == 0 else 3 elif encoder_class == encoder.AlphanumericEncoder: - new_length = (length + 1) % 2 - cost = 6 if length == 0 else 5 + new_length = (unfilled_length + 1) % 2 + cost = 6 if unfilled_length == 0 else 5 elif encoder_class == encoder.ByteEncoder: new_length = 0 cost = 8 @@ -127,9 +128,9 @@ def _compute_costs(self, data): new_length = 0 cost = encoders[new_mode].length(data[n], character_count_indicator_length) - if self.dp[n][mode][length] + cost < self.dp[n + 1][new_mode][new_length]: - self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][length] + cost - self.parents[n + 1][new_mode][new_length] = (n, mode, length) + if self.dp[n][mode][unfilled_length] + cost < self.dp[n + 1][new_mode][new_length]: + self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost + self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length) # print("=======") # print(self.dp[len(data)]) @@ -141,16 +142,16 @@ def _find_best(self, data): data (str): The data to encode Returns: - tuple: The best index as tuple (n, mode, length). + tuple: The best index as tuple (n, mode, unfilled_length). """ best = self.INF best_index = (-1, -1) for mode in range(4): - for length in range(3): - if self.dp[len(data)][mode][length] < best: - best = self.dp[len(data)][mode][length] - best_index = (len(data), mode, length) + for unfilled_length in range(3): + if self.dp[len(data)][mode][unfilled_length] < best: + best = self.dp[len(data)][mode][unfilled_length] + best_index = (len(data), mode, unfilled_length) return best_index def _reconstruct_path(self, best_index): From 88d177825c43e0307f55e6b8aafa7ac15da3ccab Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Tue, 23 Aug 2022 10:27:48 +0900 Subject: [PATCH 093/113] doc: Update docstrings --- src/rmqrcode/segments.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 2d8bbcb..35cce33 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -57,6 +57,9 @@ def compute(self, data, version): Returns: list: The list of segments. + Raises: + rmqrcode.DataTooLongError: If the data is too long to encode. + """ if len(data) > self.MAX_CHARACTER: raise DataTooLongError() From 643361262650d3da2c758c33c0eb75bc548e7678 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 25 Aug 2022 13:46:11 +0900 Subject: [PATCH 094/113] chore: Remove debug prints --- src/rmqrcode/segments.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 35cce33..1ca5b40 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -80,7 +80,7 @@ def _compute_costs(self, data): and the remainder bits length is `unfilled_length`. Args: - data (str): The data to encode + data (str): The data to encode. Returns: void @@ -93,9 +93,6 @@ def _compute_costs(self, data): self.parents[0][mode][0] = (0, 0, 0) for n in range(0, len(data)): - # print("----") - # print(f"{n} -> {n+1}") - # print(self.dp[n]) for mode in range(4): for unfilled_length in range(3): if self.dp[n][mode][unfilled_length] == self.INF: @@ -135,14 +132,11 @@ def _compute_costs(self, data): self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length) - # print("=======") - # print(self.dp[len(data)]) - def _find_best(self, data): """Find the index which has the minimum costs. Args: - data (str): The data to encode + data (str): The data to encode. Returns: tuple: The best index as tuple (n, mode, unfilled_length). @@ -164,7 +158,7 @@ def _reconstruct_path(self, best_index): best_index: The best index computed by self._find_best(). Returns: - list: The path of minimum cost in the dynamic programming table + list: The path of minimum cost in the dynamic programming table. """ path = [] From c5d7cbef6498b3a23fa5ad60e01ce4daefe26da3 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 25 Aug 2022 18:38:53 +0900 Subject: [PATCH 095/113] doc: Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b67869..2f81c5f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ optional arguments: Strategy how to determine rMQR Code size. ``` -### Generate rMQR Code +### Generate rMQR Code in scripts Alternatively, you can also use in python scripts: ```py from rmqrcode import rMQR @@ -126,6 +126,14 @@ The value for `encoder_class` is listed in the below table. |Byte|ByteEncoder|Any| |Kanji|KanjiEncoder|from 0x8140 to 0x9FFC, from 0xE040 to 0xEBBF in Shift JIS value| +### Optimal Segmentation +The `rMQR.fit` method mentioned above computes the optimal segmentation. +For example, the data "123Abc" is divided into the following two segments. + +|Segment No.|Data|Encoding Mode| +|Segment1|123|Numeric| +|Segment2|Abc|Byte| + ## 🤝 Contributing Any suggestions are welcome! If you are interesting in contributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). From d0482efab971101eba7207a24b5e24149357bac6 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 25 Aug 2022 18:40:14 +0900 Subject: [PATCH 096/113] doc: Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f81c5f..0776675 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ For example, the data "123Abc" is divided into the following two segments. |Segment1|123|Numeric| |Segment2|Abc|Byte| +In the case of other segmentation like "123A bc", the length of the bit string after +encoding will be longer than the above optimal case. ## 🤝 Contributing Any suggestions are welcome! If you are interesting in contributing, please read [CONTRIBUTING](https://github.com/OUDON/rmqrcode-python/blob/develop/CONTRIBUTING.md). From 54f2b9cfc8127f9fe9f042d9c6eced7b0a786c79 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 25 Aug 2022 18:42:35 +0900 Subject: [PATCH 097/113] doc: Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0776675..a9760fc 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ The `rMQR.fit` method mentioned above computes the optimal segmentation. For example, the data "123Abc" is divided into the following two segments. |Segment No.|Data|Encoding Mode| +|-|-|-| |Segment1|123|Numeric| |Segment2|Abc|Byte| From f7a84a609b0fbe4c6383275c4c2c6096aeafdb9c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Thu, 25 Aug 2022 18:44:52 +0900 Subject: [PATCH 098/113] doc: Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b67869..1c00e48 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The `fit_strategy` parameter is enum value of `rmqrcode.FitStrategy` to specify - **`FitStrategy.MINIMIZE_HEIGHT`**: Try to minimize height. - **`FitStrategy.BALANCED`**: Try to keep balance of width and height. -Here is an example of images genereated by each fit strategies for data `Test test test`: +Here is an example of images generated by each fit strategies for data `Test test test`: ![Example of fit strategies](https://user-images.githubusercontent.com/14174940/175759120-7fb5ec71-c258-4646-9b91-6865b3eeac3f.png) ### Save as image From 19bb3768f98d2e1c9d64a1be22a2125ecf0ce5bb Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 28 Aug 2022 08:21:16 +0900 Subject: [PATCH 099/113] fix: Fix issue KanjiEncoder.is_invalid_characters raises UnicodeEncodeError --- src/rmqrcode/encoder/kanji_encoder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py index 8346807..4971b42 100644 --- a/src/rmqrcode/encoder/kanji_encoder.py +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -33,7 +33,10 @@ def length(cls, data, character_count_indicator_length): @classmethod def is_valid_characters(cls, data): for c in data: - shift_jis = c.encode("shift_jis") + try: + shift_jis = c.encode("shift_jis") + except UnicodeEncodeError: + return False if len(shift_jis) < 2: return False hex_value = shift_jis[0] * 256 + shift_jis[1] From 26b97ade459c3ede17880a870b5198ad08be476f Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 28 Aug 2022 13:38:49 +0900 Subject: [PATCH 100/113] feat: Implement characters_num --- src/rmqrcode/encoder/alphanumeric_encoder.py | 4 ++++ src/rmqrcode/encoder/byte_encoder.py | 4 ++++ src/rmqrcode/encoder/encoder_base.py | 7 ++++++- src/rmqrcode/encoder/kanji_encoder.py | 4 ++++ src/rmqrcode/encoder/numeric_encoder.py | 4 ++++ tests/encoder/alphanumeric_encoder_test.py | 1 + tests/encoder/byte_encoder_test.py | 18 ++++++++++++++++++ tests/encoder/kanji_encoder_test.py | 1 + tests/encoder/numeric_encoder_test.py | 1 + 9 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/encoder/byte_encoder_test.py diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py index 53e0c41..2a0839f 100644 --- a/src/rmqrcode/encoder/alphanumeric_encoder.py +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -83,6 +83,10 @@ def length(cls, data, character_count_indicator_length): len(cls.mode_indicator()) + character_count_indicator_length + 11 * (len(data) // 2) + 6 * (len(data) % 2) ) + @classmethod + def characters_num(cls, data): + return len(data) + @classmethod def is_valid_characters(cls, data): return bool(re.match(r"^[0-9A-Z\s\$\%\*\+\-\.\/\:]*$", data)) diff --git a/src/rmqrcode/encoder/byte_encoder.py b/src/rmqrcode/encoder/byte_encoder.py index 2f707e5..99a9518 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -18,6 +18,10 @@ def _encoded_bits(cls, s): def length(cls, data, character_count_indicator_length): return len(cls.mode_indicator()) + character_count_indicator_length + 8 * len(data.encode("utf-8")) + @classmethod + def characters_num(cls, data): + return len(data.encode("utf-8")) + @classmethod def is_valid_characters(cls, data): return True # Any characters can encode in the Byte Mode diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index cedfc07..f9e607a 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -36,7 +36,7 @@ def encode(cls, data, character_count_indicator_length): raise IllegalCharacterError res = cls.mode_indicator() - res += bin(len(data))[2:].zfill(character_count_indicator_length) + res += bin(cls.characters_num(data))[2:].zfill(character_count_indicator_length) res += cls._encoded_bits(data) return res @@ -71,6 +71,11 @@ def length(cls, data): """ raise NotImplementedError() + @classmethod + @abstractmethod + def characters_num(cls, data): + raise NotImplementedError() + @classmethod @abstractmethod def is_valid_characters(cls, data): diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py index 4971b42..1d22240 100644 --- a/src/rmqrcode/encoder/kanji_encoder.py +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -30,6 +30,10 @@ def _encoded_bits(cls, data): def length(cls, data, character_count_indicator_length): return len(cls.mode_indicator()) + character_count_indicator_length + 13 * len(data) + @classmethod + def characters_num(cls, data): + return len(data.encode("shift_jis"))//2 + @classmethod def is_valid_characters(cls, data): for c in data: diff --git a/src/rmqrcode/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py index f9f8a8e..208e7f5 100644 --- a/src/rmqrcode/encoder/numeric_encoder.py +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -39,6 +39,10 @@ def length(cls, data, character_count_indicator_length): r = 7 return len(cls.mode_indicator()) + character_count_indicator_length + 10 * (len(data) // 3) + r + @classmethod + def characters_num(cls, data): + return len(data) + @classmethod def is_valid_characters(cls, data): return bool(re.match(r"^[0-9]*$", data)) diff --git a/tests/encoder/alphanumeric_encoder_test.py b/tests/encoder/alphanumeric_encoder_test.py index c9576f4..22fffa7 100644 --- a/tests/encoder/alphanumeric_encoder_test.py +++ b/tests/encoder/alphanumeric_encoder_test.py @@ -19,3 +19,4 @@ def test_length(self): def test_is_valid_characters(self): assert AlphanumericEncoder.is_valid_characters("AC-42") is True assert AlphanumericEncoder.is_valid_characters("abc123") is False + assert AlphanumericEncoder.is_valid_characters("📌") is False \ No newline at end of file diff --git a/tests/encoder/byte_encoder_test.py b/tests/encoder/byte_encoder_test.py new file mode 100644 index 0000000..2dfa4e4 --- /dev/null +++ b/tests/encoder/byte_encoder_test.py @@ -0,0 +1,18 @@ +from rmqrcode.encoder import ByteEncoder, IllegalCharacterError + +import pytest + + +class TestNumericEncoder: + def test_encode(self): + encoded = ByteEncoder.encode("📌", 5) + assert encoded == "0110010011110000100111111001001110001100" + + def test_length(self): + encoded_length = ByteEncoder.length("📌", 5) + assert encoded_length is 40 + + def test_is_valid_characters(self): + assert ByteEncoder.is_valid_characters("0123456789") is True + assert ByteEncoder.is_valid_characters("A1234!678@") is True + assert ByteEncoder.is_valid_characters("📌") is True diff --git a/tests/encoder/kanji_encoder_test.py b/tests/encoder/kanji_encoder_test.py index 4c0e626..3b3ca1c 100644 --- a/tests/encoder/kanji_encoder_test.py +++ b/tests/encoder/kanji_encoder_test.py @@ -19,3 +19,4 @@ def test_length(self): def test_is_valid_characters(self): assert KanjiEncoder.is_valid_characters("点茗") is True assert KanjiEncoder.is_valid_characters("abc") is False + assert KanjiEncoder.is_valid_characters("📌") is False \ No newline at end of file diff --git a/tests/encoder/numeric_encoder_test.py b/tests/encoder/numeric_encoder_test.py index 460ae78..4870e19 100644 --- a/tests/encoder/numeric_encoder_test.py +++ b/tests/encoder/numeric_encoder_test.py @@ -19,3 +19,4 @@ def test_length(self): def test_is_valid_characters(self): assert NumericEncoder.is_valid_characters("0123456789") is True assert NumericEncoder.is_valid_characters("A1234!678@") is False + assert NumericEncoder.is_valid_characters("📌") is False From a15f0e783934a7d9d26efdd4684cd6cd16c3d103 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 28 Aug 2022 14:45:05 +0900 Subject: [PATCH 101/113] doc: Add docstrings --- src/rmqrcode/encoder/encoder_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index f9e607a..ca5c75d 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -74,6 +74,15 @@ def length(cls, data): @classmethod @abstractmethod def characters_num(cls, data): + """ Returns the number of the characters of the data. + + Args: + data (str): The data to encode. + + Returns: + int: The number of the characters of the data. + + """ raise NotImplementedError() @classmethod From 927377ae6cd31714aa0193bf054e599eef84e9ef Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 28 Aug 2022 14:47:39 +0900 Subject: [PATCH 102/113] chore: Apply formatter --- src/rmqrcode/encoder/encoder_base.py | 2 +- src/rmqrcode/encoder/kanji_encoder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rmqrcode/encoder/encoder_base.py b/src/rmqrcode/encoder/encoder_base.py index ca5c75d..a5220fd 100644 --- a/src/rmqrcode/encoder/encoder_base.py +++ b/src/rmqrcode/encoder/encoder_base.py @@ -74,7 +74,7 @@ def length(cls, data): @classmethod @abstractmethod def characters_num(cls, data): - """ Returns the number of the characters of the data. + """Returns the number of the characters of the data. Args: data (str): The data to encode. diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py index 1d22240..ba5fc65 100644 --- a/src/rmqrcode/encoder/kanji_encoder.py +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -32,7 +32,7 @@ def length(cls, data, character_count_indicator_length): @classmethod def characters_num(cls, data): - return len(data.encode("shift_jis"))//2 + return len(data.encode("shift_jis")) // 2 @classmethod def is_valid_characters(cls, data): From 17e4f22039eb49b3c4348cf1212e1688f8472d30 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 29 Aug 2022 10:06:21 +0900 Subject: [PATCH 103/113] chore: Insert a new line at last line --- tests/encoder/alphanumeric_encoder_test.py | 2 +- tests/encoder/kanji_encoder_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/encoder/alphanumeric_encoder_test.py b/tests/encoder/alphanumeric_encoder_test.py index 22fffa7..1f621a2 100644 --- a/tests/encoder/alphanumeric_encoder_test.py +++ b/tests/encoder/alphanumeric_encoder_test.py @@ -19,4 +19,4 @@ def test_length(self): def test_is_valid_characters(self): assert AlphanumericEncoder.is_valid_characters("AC-42") is True assert AlphanumericEncoder.is_valid_characters("abc123") is False - assert AlphanumericEncoder.is_valid_characters("📌") is False \ No newline at end of file + assert AlphanumericEncoder.is_valid_characters("📌") is False diff --git a/tests/encoder/kanji_encoder_test.py b/tests/encoder/kanji_encoder_test.py index 3b3ca1c..debb7fd 100644 --- a/tests/encoder/kanji_encoder_test.py +++ b/tests/encoder/kanji_encoder_test.py @@ -19,4 +19,4 @@ def test_length(self): def test_is_valid_characters(self): assert KanjiEncoder.is_valid_characters("点茗") is True assert KanjiEncoder.is_valid_characters("abc") is False - assert KanjiEncoder.is_valid_characters("📌") is False \ No newline at end of file + assert KanjiEncoder.is_valid_characters("📌") is False From ea7b4eb89fa9d7cd0329acaa4c51407f99805fef Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Mon, 29 Aug 2022 10:09:05 +0900 Subject: [PATCH 104/113] chore: Bumps to 0.3.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index cf930d6..94f2368 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = rmqrcode -version = 0.2.0 +version = 0.3.0 author = Takahiro Tomita author_email = ttp8101@gmail.com description = An rMQR Code Generetor From ae2c2dbc1d1055de3e784dd10ca7a16960950484 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 11:42:40 +0900 Subject: [PATCH 105/113] fix: Fix invalid segmentation issue --- src/rmqrcode/segments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 1ca5b40..c5801c8 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -116,7 +116,8 @@ def _compute_costs(self, data): cost = 6 if unfilled_length == 0 else 5 elif encoder_class == encoder.ByteEncoder: new_length = 0 - cost = 8 + cost = 8 * len(data[n].encode('utf-8')) + print(data[n].encode('utf-8')) elif encoder_class == encoder.KanjiEncoder: new_length = 0 cost = 13 From 4d14596313e7e2a072b33136c468eab83885136c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 11:43:09 +0900 Subject: [PATCH 106/113] feat: Add more testcases into segments_test.py --- tests/segments_test.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/segments_test.py b/tests/segments_test.py index 073d230..4f68da2 100644 --- a/tests/segments_test.py +++ b/tests/segments_test.py @@ -4,7 +4,7 @@ class TestSegments: - def test_can_optimize_segments(self): + def test_can_optimize_segments_numeric_and_byte(self): optimizer = SegmentOptimizer() segments = optimizer.compute("123Abc", "R7x43") assert segments == [ @@ -12,6 +12,42 @@ def test_can_optimize_segments(self): {"data": "Abc", "encoder_class": encoder.ByteEncoder}, ] + def test_can_optimize_segments_alphanumeric_and_kanji(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("17:30集合", "R7x59") + assert segments == [ + {"data": "17:30", "encoder_class": encoder.AlphanumericEncoder}, + {"data": "集合", "encoder_class": encoder.KanjiEncoder}, + ] + + def test_can_optimize_segments_numeric_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123456", "R7x59") + assert segments == [ + {"data": "123456", "encoder_class": encoder.NumericEncoder}, + ] + + def test_can_optimize_segments_alphanumeric_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("HTTPS://", "R7x59") + assert segments == [ + {"data": "HTTPS://", "encoder_class": encoder.AlphanumericEncoder}, + ] + + def test_can_optimize_segments_byte_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("1+zY!a:K", "R7x59") + assert segments == [ + {"data": "1+zY!a:K", "encoder_class": encoder.ByteEncoder}, + ] + + def test_can_optimize_segments_kanji_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("漢字", "R7x59") + assert segments == [ + {"data": "漢字", "encoder_class": encoder.KanjiEncoder}, + ] + def test_compute_length(self): optimizer = SegmentOptimizer() segments = optimizer.compute("123Abc", "R7x43") From 7921e9ceac0a78f5220a4e9c1c48735bfa39dd18 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 11:44:16 +0900 Subject: [PATCH 107/113] chore: Apply formatter --- src/rmqrcode/segments.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index c5801c8..dbfec97 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -116,8 +116,7 @@ def _compute_costs(self, data): cost = 6 if unfilled_length == 0 else 5 elif encoder_class == encoder.ByteEncoder: new_length = 0 - cost = 8 * len(data[n].encode('utf-8')) - print(data[n].encode('utf-8')) + cost = 8 * len(data[n].encode("utf-8")) elif encoder_class == encoder.KanjiEncoder: new_length = 0 cost = 13 From 5395f6ce6e48c1920b8c4d519666e0d2f074799c Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 16:44:35 +0900 Subject: [PATCH 108/113] feat: SegmentOptimizer#compute raises DataTooLongError --- src/rmqrcode/rmqrcode.py | 37 +++++++++++++++++++------------------ src/rmqrcode/segments.py | 19 ++++++++++++++----- tests/segments_test.py | 21 +++++++++++++-------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 74dc670..8330a8a 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -82,23 +82,24 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): logger.debug("Select rMQR Code version") for version_name, qr_version in DataCapacities.items(): optimizer = qr_segments.SegmentOptimizer() - optimized_segments = optimizer.compute(data, version_name) - data_length = qr_segments.compute_length(optimized_segments, version_name) - - if data_length <= qr_version["number_of_data_bits"][ecc]: - width, height = qr_version["width"], qr_version["height"] - if width not in determined_width and height not in determined_height: - determined_width.add(width) - determined_height.add(height) - ok_versions.append( - { - "version": version_name, - "width": width, - "height": height, - "segments": optimized_segments, - } - ) - logger.debug(f"ok: {version_name}") + try: + optimized_segments = optimizer.compute(data, version_name, ecc) + except DataTooLongError: + continue + + width, height = qr_version["width"], qr_version["height"] + if width not in determined_width and height not in determined_height: + determined_width.add(width) + determined_height.add(height) + ok_versions.append( + { + "version": version_name, + "width": width, + "height": height, + "segments": optimized_segments, + } + ) + logger.debug(f"ok: {version_name}") if len(ok_versions) == 0: raise DataTooLongError("The data is too long.") @@ -128,7 +129,7 @@ def sort_key(x): def _optimized_segments(self, data): optimizer = qr_segments.SegmentOptimizer() - return optimizer.compute(data, self.version_name()) + return optimizer.compute(data, self.version_name(), self._error_correction_level) def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index dbfec97..6986543 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -1,6 +1,7 @@ from . import encoder from .errors import DataTooLongError from .format.rmqr_versions import rMQRVersions +from .format.data_capacities import DataCapacities encoders = [ encoder.NumericEncoder, @@ -47,12 +48,13 @@ def __init__(self): self.dp = [[[self.INF for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] self.parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)] - def compute(self, data, version): + def compute(self, data, version, ecc): """Computes the optimize segmentation for the given data. Args: data (str): The data to encode. version (str): The version name. + ecc (rmqrcode.ErrorCorrectionLevel): The error correction level. Returns: list: The list of segments. @@ -66,8 +68,11 @@ def compute(self, data, version): self.qr_version = rMQRVersions[version] self._compute_costs(data) - best_index = self._find_best(data) - path = self._reconstruct_path(best_index) + best = self._find_best(data) + if best["cost"] > DataCapacities[version]["number_of_data_bits"][ecc]: + raise DataTooLongError + + path = self._reconstruct_path(best["index"]) segments = self._compute_segments(path, data) return segments @@ -139,7 +144,8 @@ def _find_best(self, data): data (str): The data to encode. Returns: - tuple: The best index as tuple (n, mode, unfilled_length). + dict: The dict object includes "cost" and "index". The "cost" is the value of minimum cost. + The "index" is the index of the dp table as a tuple (n, mode, unfilled_length). """ best = self.INF @@ -149,7 +155,10 @@ def _find_best(self, data): if self.dp[len(data)][mode][unfilled_length] < best: best = self.dp[len(data)][mode][unfilled_length] best_index = (len(data), mode, unfilled_length) - return best_index + return { + "cost": best, + "index": best_index + } def _reconstruct_path(self, best_index): """Reconstructs the path. diff --git a/tests/segments_test.py b/tests/segments_test.py index 4f68da2..552dd27 100644 --- a/tests/segments_test.py +++ b/tests/segments_test.py @@ -1,12 +1,12 @@ from rmqrcode.segments import SegmentOptimizer, compute_length -from rmqrcode import encoder +from rmqrcode import encoder, ErrorCorrectionLevel, DataTooLongError import pytest class TestSegments: def test_can_optimize_segments_numeric_and_byte(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("123Abc", "R7x43") + segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M) assert segments == [ {"data": "123", "encoder_class": encoder.NumericEncoder}, {"data": "Abc", "encoder_class": encoder.ByteEncoder}, @@ -14,7 +14,7 @@ def test_can_optimize_segments_numeric_and_byte(self): def test_can_optimize_segments_alphanumeric_and_kanji(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("17:30集合", "R7x59") + segments = optimizer.compute("17:30集合", "R7x59", ErrorCorrectionLevel.M) assert segments == [ {"data": "17:30", "encoder_class": encoder.AlphanumericEncoder}, {"data": "集合", "encoder_class": encoder.KanjiEncoder}, @@ -22,33 +22,38 @@ def test_can_optimize_segments_alphanumeric_and_kanji(self): def test_can_optimize_segments_numeric_only(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("123456", "R7x59") + segments = optimizer.compute("123456", "R7x59", ErrorCorrectionLevel.M) assert segments == [ {"data": "123456", "encoder_class": encoder.NumericEncoder}, ] def test_can_optimize_segments_alphanumeric_only(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("HTTPS://", "R7x59") + segments = optimizer.compute("HTTPS://", "R7x59", ErrorCorrectionLevel.M) assert segments == [ {"data": "HTTPS://", "encoder_class": encoder.AlphanumericEncoder}, ] def test_can_optimize_segments_byte_only(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("1+zY!a:K", "R7x59") + segments = optimizer.compute("1+zY!a:K", "R7x59", ErrorCorrectionLevel.M) assert segments == [ {"data": "1+zY!a:K", "encoder_class": encoder.ByteEncoder}, ] def test_can_optimize_segments_kanji_only(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("漢字", "R7x59") + segments = optimizer.compute("漢字", "R7x59", ErrorCorrectionLevel.M) assert segments == [ {"data": "漢字", "encoder_class": encoder.KanjiEncoder}, ] + def test_optimize_segments_raises_data_too_long_error(self): + optimizer = SegmentOptimizer() + with pytest.raises(DataTooLongError) as e: + segments = optimizer.compute("a" * 12, "R7x59", ErrorCorrectionLevel.M) + def test_compute_length(self): optimizer = SegmentOptimizer() - segments = optimizer.compute("123Abc", "R7x43") + segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M) assert compute_length(segments, "R7x43") is 47 From 24abaaf018c834ac02c486409088b2778636d515 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 16:45:03 +0900 Subject: [PATCH 109/113] chore: Apply formatter --- src/rmqrcode/segments.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 6986543..61d2763 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -1,7 +1,7 @@ from . import encoder from .errors import DataTooLongError -from .format.rmqr_versions import rMQRVersions from .format.data_capacities import DataCapacities +from .format.rmqr_versions import rMQRVersions encoders = [ encoder.NumericEncoder, @@ -155,10 +155,7 @@ def _find_best(self, data): if self.dp[len(data)][mode][unfilled_length] < best: best = self.dp[len(data)][mode][unfilled_length] best_index = (len(data), mode, unfilled_length) - return { - "cost": best, - "index": best_index - } + return {"cost": best, "index": best_index} def _reconstruct_path(self, best_index): """Reconstructs the path. From c972c579eb5566fac751669ef250cb5bed087207 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 17:38:09 +0900 Subject: [PATCH 110/113] refactor: Split `SegmentOptimizer#_compute_costs` into three methods --- src/rmqrcode/segments.py | 73 +++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 61d2763..4b1019f 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -107,36 +107,63 @@ def _compute_costs(self, data): if not encoders[new_mode].is_valid_characters(data[n]): continue - encoder_class = encoders[new_mode] - character_count_indicator_length = self.qr_version["character_count_indicator_length"][ - encoder_class - ] if new_mode == mode: - # Keep the mode - if encoder_class == encoder.NumericEncoder: - new_length = (unfilled_length + 1) % 3 - cost = 4 if unfilled_length == 0 else 3 - elif encoder_class == encoder.AlphanumericEncoder: - new_length = (unfilled_length + 1) % 2 - cost = 6 if unfilled_length == 0 else 5 - elif encoder_class == encoder.ByteEncoder: - new_length = 0 - cost = 8 * len(data[n].encode("utf-8")) - elif encoder_class == encoder.KanjiEncoder: - new_length = 0 - cost = 13 + cost, new_length = self._compute_new_state_without_mode_changing(data[n], new_mode, unfilled_length) else: - # Change the mode - if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: - new_length = 1 - elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: - new_length = 0 - cost = encoders[new_mode].length(data[n], character_count_indicator_length) + cost, new_length = self._compute_new_state_with_mode_changing(data[n], new_mode, unfilled_length) if self.dp[n][mode][unfilled_length] + cost < self.dp[n + 1][new_mode][new_length]: self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length) + def _compute_new_state_without_mode_changing(self, character, new_mode, unfilled_length): + """Computes the new state without mode changing. + + Args: + character (str): The current character. Assume this as one length string. + new_mode (int): The state of the new mode. + unfilled_length (int): The state of the current unfilled_length. + + Returns: + tuple: (cost, new_length). + + """ + encoder_class = encoders[new_mode] + if encoder_class == encoder.NumericEncoder: + new_length = (unfilled_length + 1) % 3 + cost = 4 if unfilled_length == 0 else 3 + elif encoder_class == encoder.AlphanumericEncoder: + new_length = (unfilled_length + 1) % 2 + cost = 6 if unfilled_length == 0 else 5 + elif encoder_class == encoder.ByteEncoder: + new_length = 0 + cost = 8 * len(character.encode("utf-8")) + elif encoder_class == encoder.KanjiEncoder: + new_length = 0 + cost = 13 + return (cost, new_length) + + def _compute_new_state_with_mode_changing(self, character, new_mode, unfilled_length): + """Computes the new state with mode changing. + + Args: + character (str): The current character. Assume this as one length string. + new_mode (int): The state of the new mode. + unfilled_length (int): The state of the current unfilled_length. + + Returns: + tuple: (cost, new_length). + + """ + encoder_class = encoders[new_mode] + character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] + if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: + new_length = 1 + elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: + new_length = 0 + cost = encoder_class.length(character, character_count_indicator_length) + return (cost, new_length) + def _find_best(self, data): """Find the index which has the minimum costs. From 8b2a1c23239568a8c38c0e9c88b4c04054f43e5e Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 17:40:27 +0900 Subject: [PATCH 111/113] chore: Apply formatter --- src/rmqrcode/segments.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 4b1019f..4661aa3 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -108,9 +108,13 @@ def _compute_costs(self, data): continue if new_mode == mode: - cost, new_length = self._compute_new_state_without_mode_changing(data[n], new_mode, unfilled_length) + cost, new_length = self._compute_new_state_without_mode_changing( + data[n], new_mode, unfilled_length + ) else: - cost, new_length = self._compute_new_state_with_mode_changing(data[n], new_mode, unfilled_length) + cost, new_length = self._compute_new_state_with_mode_changing( + data[n], new_mode, unfilled_length + ) if self.dp[n][mode][unfilled_length] + cost < self.dp[n + 1][new_mode][new_length]: self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost From 7da583474a9cea0a1dad43756969db98f5864bf6 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 19:08:09 +0900 Subject: [PATCH 112/113] chore: Update docstrings --- src/rmqrcode/segments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py index 4661aa3..02aa436 100644 --- a/src/rmqrcode/segments.py +++ b/src/rmqrcode/segments.py @@ -121,7 +121,7 @@ def _compute_costs(self, data): self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length) def _compute_new_state_without_mode_changing(self, character, new_mode, unfilled_length): - """Computes the new state without mode changing. + """Computes the new state values without mode changing. Args: character (str): The current character. Assume this as one length string. @@ -148,7 +148,7 @@ def _compute_new_state_without_mode_changing(self, character, new_mode, unfilled return (cost, new_length) def _compute_new_state_with_mode_changing(self, character, new_mode, unfilled_length): - """Computes the new state with mode changing. + """Computes the new state values with mode changing. Args: character (str): The current character. Assume this as one length string. From d551f2d7f9a17f4015d0c1fe283a7ce085af8693 Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Sun, 4 Sep 2022 19:11:48 +0900 Subject: [PATCH 113/113] chore: Bumps to v0.3.1 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 94f2368..59f03bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = rmqrcode -version = 0.3.0 +version = 0.3.1 author = Takahiro Tomita author_email = ttp8101@gmail.com description = An rMQR Code Generetor