From 3855bf3934d2e0a86f1b3500512658aa12d1272d Mon Sep 17 00:00:00 2001 From: Takahiro Tomita Date: Fri, 12 Aug 2022 07:13:26 +0900 Subject: [PATCH 01/64] 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 02/64] 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 03/64] 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 04/64] 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 05/64] 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 06/64] 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 07/64] 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 08/64] 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 09/64] 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 10/64] 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 11/64] 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 12/64] 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 13/64] 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 14/64] 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 15/64] 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 16/64] 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 17/64] 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 18/64] 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 19/64] 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 20/64] 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 21/64] 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 22/64] 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 23/64] 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 24/64] 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 25/64] 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 26/64] 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 27/64] 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 28/64] 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 29/64] 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 30/64] 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 31/64] 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 32/64] 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 33/64] 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 34/64] 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 35/64] 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 36/64] 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 37/64] 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 38/64] 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 39/64] 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 40/64] 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 41/64] 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 42/64] 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 43/64] 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 44/64] 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 45/64] 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 46/64] 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 47/64] 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 48/64] 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 49/64] 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 50/64] 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 51/64] 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 52/64] 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 53/64] 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 54/64] 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 55/64] 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 56/64] 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 57/64] 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 58/64] 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 59/64] 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 60/64] 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 61/64] 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 62/64] 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 63/64] 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 64/64] 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