diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..91abb11 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d9d10dd..472aafc 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -1,13 +1,11 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python application +name: pytest on: - push: - branches: [ "main" ] pull_request: - branches: [ "develop" ] + branches: [ "main", "develop" ] permissions: contents: read @@ -26,15 +24,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - pip install . + pip install .[dev] if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 + - name: Lint run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + make lint - name: Test with pytest run: | python -m pytest diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 613d028..ec70354 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -37,4 +37,3 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..40429cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing to rmqrcode-python +Thank you for interesting in contributing to rmqrcode-python! Any suggestions are welcome. + +## Style Guides + +### Docstrings +This project uses [Google-Style format](https://google.github.io/styleguide/pyguide.html#381-docstrings) docstrings. + +### Git Commit Message +Consider starting commit message with one of the following prefixes. +- `feat:` : New feature +- `fix:` : Bug fix +- `refactor:` : Refactoring +- `chore:` : Little things +- `ci`: CI +- `doc:` : Documentation + +## Pull Requests +Before make a pull request, please do the following. +1. `make format` +2. `make lint` +3. `python -m pytest` and make sure all tests are passed. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..985623c --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: install +install: + pip install -e ".[dev]" + +.PHONY: uninstall +uninstall: + yes Y | pip uninstall rmqrcode + +.PHONY: test +test: + python -m pytest + +.PHONY: lint +lint: + flake8 src + isort --check --diff src + black --check src + +.PHONY: format +format: + isort src + black src diff --git a/README.md b/README.md index 2e2a773..9ecbab0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,55 @@ -# An rMQR Code Generator -![Lorem_ipsum](https://user-images.githubusercontent.com/14174940/171996095-4707be09-506e-4ef2-ab90-9942d6efc2ed.png) +# Rectangular Micro QR Code (rMQR Code) Generator +![reop-url](https://user-images.githubusercontent.com/14174940/172978619-accbf9d0-9dd8-4b19-b47e-ad139a68dcc9.png) -This is an rMQR Code image generator implemented in Python. -## Important Notice -Please verify an image generated by this software whether it can decode correctly before use. +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). -## Basic Usage +[![pytest](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml/badge.svg?branch=main)](https://github.com/OUDON/rmqrcode-python/actions/workflows/python-app.yml) +![PyPI](https://img.shields.io/pypi/v/rmqrcode?color=blue) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rmqrcode) +![PyPI - Downloads](https://img.shields.io/pypi/dm/rmqrcode?color=orange) + +## 🎮 Online Demo Site +You can try this online: https://rmqr.oudon.xyz . + +## 📌 Notice +- Please verify an image generated by this software whether it can decode correctly before use. +- Because this is in early stage, QR Code readers may have not been supported rMQR Code yet. + + +## 🚀 Installation +``` +pip install rmqrcode +``` + + +## 📕 Basic Usage +### CLI +Generate an rMQR Code image from your command line, use `rmqr` command: +```sh +rmqr 'Text data' 'my_qr.png' +``` + +See the help to list the options: +```sh +➜ rmqr -h +usage: rmqr [-h] [--ecc {M,H}] [--version VERSION] [--fit-strategy {min_width,min_height,balanced}] + DATA OUTPUT + +positional arguments: + DATA Data to encode. + OUTPUT Output file path + +optional arguments: + -h, --help show this help message and exit + --ecc {M,H} Error correction level. (default: M) + --version VERSION rMQR Code version like 'R11x139'. + --fit-strategy {min_width,min_height,balanced} + Strategy how to determine rMQR Code size. +``` + +### Generate rMQR Code in scripts +Alternatively, you can also use in python scripts: ```py from rmqrcode import rMQR import rmqrcode @@ -19,26 +62,92 @@ qr = rMQR.fit( ) ``` -The `ecc` parameter is an enum value of rmqrcode.ErrorCorrectionLevel to select error correction level. The following values are available: +The `ecc` parameter is an enum value of `rmqrcode.ErrorCorrectionLevel` to select error correction level. The following values are available: - **`ErrorCorrectionLevel.M`**: Approx. 15% Recovery Capacity. - **`ErrorCorrectionLevel.H`**: Approx. 30% Recovery Capacity. -The `fit_strategy` parameter is enum value of rmqrcode.FitStrategy to specify how to determine size of rMQR Code. The following values are available: +The `fit_strategy` parameter is enum value of `rmqrcode.FitStrategy` to specify how to determine size of rMQR Code. The following values are available: - **`FitStrategy.MINIMIZE_WIDTH`**: Try to minimize width. - **`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`: -![Example of fit strategies](https://user-images.githubusercontent.com/14174940/172822478-4f2b5fb8-49bd-464f-b6b2-c7347f71cbf5.png) +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 +```py +from rmqrcode import QRImage + +image = QRImage(qr, module_size=8) +image.show() +image.save("my_qr.png") +``` -## Advanced Usage + +## 📙 Advanced Usage ### Select rMQR Code size manually To select rMQR Code size manually, use `rMQR()` constructor. ```py +from rmqrcode import rMQR, ErrorCorrectionLevel qr = rMQR('R11x139', ErrorCorrectionLevel.H) -qr.make("https://oudon.xyz") ``` +`R11x139` means 11 rows and 139 columns. The following table shows available combinations. + +| |27|43|59|77|99|139| +|-|:-:|:-:|:-:|:-:|:-:|:-:| +|R7|❌|✅|✅|✅|✅|✅| +|R9|❌|✅|✅|✅|✅|✅| +|R11|✅|✅|✅|✅|✅|✅| +|R13|✅|✅|✅|✅|✅|✅| +|R15|❌|✅|✅|✅|✅|✅| +|R17|❌|✅|✅|✅|✅|✅| + +### Encoding Modes and Segments + +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('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. + +|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| + +### 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| + +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). + + +## 📚 References +- [Rectangular Micro QR Code (rMQR) bar code symbology specification: ISO/IEC 23941](https://www.iso.org/standard/77404.html) +- [rMQR Code | QRcode.com | DENSO WAVE](https://www.qrcode.com/en/codes/rmqr.html) +- [Creating a QR Code step by step](https://www.nayuki.io/page/creating-a-qr-code-step-by-step) + + ---- The word "QR Code" is registered trademark of DENSO WAVE Incorporated.
http://www.denso-wave.com/qrcode/faqpatent-e.html diff --git a/example.py b/example.py index 92b3792..983c57d 100644 --- a/example.py +++ b/example.py @@ -2,10 +2,19 @@ from rmqrcode import ErrorCorrectionLevel from rmqrcode import QRImage from rmqrcode import FitStrategy +from rmqrcode import encoder import logging +try: + import numpy + import cv2 + USE_NUMPY = True +except ImportError: + USE_NUMPY = False + + def main(): data = "https://oudon.xyz" error_correction_level = ErrorCorrectionLevel.M @@ -16,16 +25,25 @@ def main(): print(qr) # Determine rMQR version manually - # version = 'R13x99' + # version = 'R7x43' # qr = rMQR(version, error_correction_level) - # qr.make(data) + # qr.add_segment("123", encoder_class=encoder.NumericEncoder) + # qr.add_segment("Abc", encoder_class=encoder.ByteEncoder) + # qr.make() # print(qr) # Save as png - image = QRImage(qr) + image = QRImage(qr, module_size=8) image.show() image.save("my_qr.png") + # Convert to numpy array + if USE_NUMPY: + img = image.get_ndarray() + cv2.imshow("img", img) + cv2.waitKey(0) + cv2.destroyAllWindows() + def _init_logger(): logger = logging.getLogger() diff --git a/pyproject.toml b/pyproject.toml index fa7093a..8969d2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ [build-system] requires = ["setuptools>=42"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 119 +exclude = "generator_polynomials.py" diff --git a/setup.cfg b/setup.cfg index 8e43ff4..59f03bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = rmqrcode -version = 0.0.7 +version = 0.3.1 author = Takahiro Tomita author_email = ttp8101@gmail.com description = An rMQR Code Generetor @@ -23,6 +23,21 @@ install_requires = [options.extras_require] dev = pytest + flake8 + isort + black [options.packages.find] -where = src \ No newline at end of file +where = src + +[options.entry_points] +console_scripts = + rmqr = rmqrcode.console:main + +[flake8] +max-line-length = 119 +extend-ignore = E203 +exclude = src/rmqrcode/format/generator_polynomials.py + +[isort] +profile=black diff --git a/src/rmqrcode/__init__.py b/src/rmqrcode/__init__.py index d9c4f19..1e05407 100644 --- a/src/rmqrcode/__init__.py +++ b/src/rmqrcode/__init__.py @@ -1,6 +1,21 @@ -from .rmqrcode import rMQR -from .rmqrcode import FitStrategy -from .rmqrcode import DataTooLongError -from .rmqrcode import IllegalVersionError -from .qr_image import QRImage +from . import encoder from .format.error_correction_level import ErrorCorrectionLevel +from .qr_image import QRImage +from .rmqrcode import ( + DataTooLongError, + FitStrategy, + IllegalVersionError, + NoSegmentError, + rMQR, +) + +__all__ = ( + "rMQR", + "DataTooLongError", + "FitStrategy", + "IllegalVersionError", + "NoSegmentError", + "QRImage", + "ErrorCorrectionLevel", + "encoder", +) diff --git a/src/rmqrcode/console.py b/src/rmqrcode/console.py new file mode 100644 index 0000000..66c7bfc --- /dev/null +++ b/src/rmqrcode/console.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +import argparse +import sys + +from rmqrcode import ( + DataTooLongError, + ErrorCorrectionLevel, + FitStrategy, + IllegalVersionError, + QRImage, + rMQR, +) + + +def _show_error_and_exit(msg): + print(msg, file=sys.stderr) + sys.exit(1) + + +def _make_qr(data, ecc, version, fit_strategy): + if version is None: + qr = rMQR.fit(data, ecc=ecc, fit_strategy=fit_strategy) + else: + try: + qr = rMQR(version, ecc) + except IllegalVersionError: + _show_error_and_exit("Error: Illegal version.") + qr.add_segment(data) + qr.make() + return qr + + +def _save_image(qr, output): + image = QRImage(qr) + try: + image.save(output) + except FileNotFoundError as e: + _show_error_and_exit(f"Error: {e}") + + +def main(): + parser = _init_argparser() + args = parser.parse_args() + + if args.ecc == "M": + ecc = ErrorCorrectionLevel.M + elif args.ecc == "H": + ecc = ErrorCorrectionLevel.H + + fit_strategy = FitStrategy.BALANCED + if args.fit_strategy == "min_width": + fit_strategy = FitStrategy.MINIMIZE_WIDTH + elif args.fit_strategy == "min_height": + fit_strategy = FitStrategy.MINIMIZE_HEIGHT + + try: + qr = _make_qr(args.DATA, ecc=ecc, version=args.version, fit_strategy=fit_strategy) + except DataTooLongError: + _show_error_and_exit("Error: The data is too long.") + + _save_image(qr, args.OUTPUT) + + +def _init_argparser(): + parser = argparse.ArgumentParser() + parser.add_argument("DATA", type=str, help="Data to encode.") + parser.add_argument("OUTPUT", type=str, help="Output file path") + parser.add_argument( + "--ecc", help="Error correction level. (default: M)", type=str, choices=["M", "H"], default="M" + ) + parser.add_argument("--version", help="rMQR Code version like 'R11x139'.") + parser.add_argument( + "--fit-strategy", + choices=["min_width", "min_height", "balanced"], + help="Strategy how to determine rMQR Code size.", + dest="fit_strategy", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/src/rmqrcode/encoder/__init__.py b/src/rmqrcode/encoder/__init__.py index e69de29..3ed1e17 100644 --- a/src/rmqrcode/encoder/__init__.py +++ b/src/rmqrcode/encoder/__init__.py @@ -0,0 +1,7 @@ +from .alphanumeric_encoder import AlphanumericEncoder +from .byte_encoder import ByteEncoder +from .encoder_base import IllegalCharacterError +from .kanji_encoder import KanjiEncoder +from .numeric_encoder import NumericEncoder + +__all__ = ("ByteEncoder", "NumericEncoder", "IllegalCharacterError", "AlphanumericEncoder", "KanjiEncoder") diff --git a/src/rmqrcode/encoder/alphanumeric_encoder.py b/src/rmqrcode/encoder/alphanumeric_encoder.py new file mode 100644 index 0000000..2a0839f --- /dev/null +++ b/src/rmqrcode/encoder/alphanumeric_encoder.py @@ -0,0 +1,92 @@ +import re + +from .encoder_base import EncoderBase + + +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 _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 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 38b7c22..99a9518 100644 --- a/src/rmqrcode/encoder/byte_encoder.py +++ b/src/rmqrcode/encoder/byte_encoder.py @@ -1,23 +1,27 @@ -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') + encoded = s.encode("utf-8") for byte in encoded: res += bin(byte)[2:].zfill(8) 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")) - @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) - return res - + @classmethod + def characters_num(cls, data): + return len(data.encode("utf-8")) - @staticmethod - def length(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 new file mode 100644 index 0000000..a5220fd --- /dev/null +++ b/src/rmqrcode/encoder/encoder_base.py @@ -0,0 +1,105 @@ +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. + + Raises: + IllegalCharacterError: If the data includes illegal character. + + """ + if not cls.is_valid_characters(data): + raise IllegalCharacterError + + res = cls.mode_indicator() + res += bin(cls.characters_num(data))[2:].zfill(character_count_indicator_length) + res += cls._encoded_bits(data) + return res + + @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() + + @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 + @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 diff --git a/src/rmqrcode/encoder/kanji_encoder.py b/src/rmqrcode/encoder/kanji_encoder.py new file mode 100644 index 0000000..ba5fc65 --- /dev/null +++ b/src/rmqrcode/encoder/kanji_encoder.py @@ -0,0 +1,49 @@ +from .encoder_base import EncoderBase, IllegalCharacterError + + +class KanjiEncoder(EncoderBase): + @classmethod + def mode_indicator(cls): + return "100" + + @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 characters_num(cls, data): + return len(data.encode("shift_jis")) // 2 + + @classmethod + def is_valid_characters(cls, data): + for c in data: + 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] + 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/encoder/numeric_encoder.py b/src/rmqrcode/encoder/numeric_encoder.py new file mode 100644 index 0000000..208e7f5 --- /dev/null +++ b/src/rmqrcode/encoder/numeric_encoder.py @@ -0,0 +1,48 @@ +import re + +from .encoder_base import EncoderBase + + +class NumericEncoder(EncoderBase): + @classmethod + def mode_indicator(cls): + return "001" + + @classmethod + 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): + 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 + + @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/src/rmqrcode/enums/color.py b/src/rmqrcode/enums/color.py index 7aeb18d..324839b 100644 --- a/src/rmqrcode/enums/color.py +++ b/src/rmqrcode/enums/color.py @@ -1,6 +1,7 @@ from enum import Enum + class Color(Enum): UNDEFINED = -1 WHITE = 0 - BLACK = 1 \ No newline at end of file + BLACK = 1 diff --git a/src/rmqrcode/enums/fit_strategy.py b/src/rmqrcode/enums/fit_strategy.py index bd006e9..80588b3 100644 --- a/src/rmqrcode/enums/fit_strategy.py +++ b/src/rmqrcode/enums/fit_strategy.py @@ -1,6 +1,7 @@ from enum import Enum + class FitStrategy(Enum): MINIMIZE_WIDTH = 0 MINIMIZE_HEIGHT = 1 - BALANCED = 2 \ No newline at end of file + BALANCED = 2 diff --git a/src/rmqrcode/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/format/alignment_pattern_coordinates.py b/src/rmqrcode/format/alignment_pattern_coordinates.py index 249698b..7b95c3f 100644 --- a/src/rmqrcode/format/alignment_pattern_coordinates.py +++ b/src/rmqrcode/format/alignment_pattern_coordinates.py @@ -5,4 +5,4 @@ 77: [25, 51], 99: [23, 49, 75], 139: [27, 55, 83, 111], -} \ No newline at end of file +} diff --git a/src/rmqrcode/format/data_capacities.py b/src/rmqrcode/format/data_capacities.py index 7cb8541..adeb1d7 100644 --- a/src/rmqrcode/format/data_capacities.py +++ b/src/rmqrcode/format/data_capacities.py @@ -1,326 +1,453 @@ from .error_correction_level import ErrorCorrectionLevel - # ISO/IEC 23941:2022 Table 6 -data_capacities = { - 'R7x43': { - 'height': 7, - 'width': 43, - 'capacity': { - 'Byte': { +DataCapacities = { + "R7x43": { + "height": 7, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 48, + ErrorCorrectionLevel.H: 24, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 5, ErrorCorrectionLevel.H: 2, }, }, }, - 'R7x59': { - 'height': 7, - 'width': 59, - 'capacity': { - 'Byte': { + "R7x59": { + "height": 7, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R7x77': { - 'height': 7, - 'width': 77, - 'capacity': { - 'Byte': { + "R7x77": { + "height": 7, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 160, + ErrorCorrectionLevel.H: 80, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 19, ErrorCorrectionLevel.H: 9, }, }, }, - 'R7x99': { - 'height': 7, - 'width': 99, - 'capacity': { - 'Byte': { + "R7x99": { + "height": 7, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 224, + ErrorCorrectionLevel.H: 112, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 27, ErrorCorrectionLevel.H: 13, }, }, }, - 'R7x139': { - 'height': 7, - 'width': 139, - 'capacity': { - 'Byte': { + "R7x139": { + "height": 7, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 352, + ErrorCorrectionLevel.H: 192, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 42, ErrorCorrectionLevel.H: 22, }, }, }, - 'R9x43': { - 'height': 9, - 'width': 43, - 'capacity': { - 'Byte': { + "R9x43": { + "height": 9, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R9x59': { - 'height': 9, - 'width': 59, - 'capacity': { - 'Byte': { + "R9x59": { + "height": 9, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 168, + ErrorCorrectionLevel.H: 88, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 20, ErrorCorrectionLevel.H: 10, }, }, }, - 'R9x77': { - 'height': 9, - 'width': 77, - 'capacity': { - 'Byte': { + "R9x77": { + "height": 9, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 248, + ErrorCorrectionLevel.H: 136, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 30, ErrorCorrectionLevel.H: 16, }, }, }, - 'R9x99': { - 'height': 9, - 'width': 99, - 'capacity': { - 'Byte': { + "R9x99": { + "height": 9, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 336, + ErrorCorrectionLevel.H: 176, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 40, ErrorCorrectionLevel.H: 20, }, }, }, - 'R9x139': { - 'height': 9, - 'width': 139, - 'capacity': { - 'Byte': { + "R9x139": { + "height": 9, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 504, + ErrorCorrectionLevel.H: 264, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 61, ErrorCorrectionLevel.H: 31, }, }, }, - 'R11x27': { - 'height': 11, - 'width': 27, - 'capacity': { - 'Byte': { + "R11x27": { + "height": 11, + "width": 27, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 56, + ErrorCorrectionLevel.H: 40, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 6, ErrorCorrectionLevel.H: 4, }, }, }, - 'R11x43': { - 'height': 11, - 'width': 43, - 'capacity': { - 'Byte': { + "R11x43": { + "height": 11, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 152, + ErrorCorrectionLevel.H: 88, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 18, ErrorCorrectionLevel.H: 10, }, }, }, - 'R11x59': { - 'height': 11, - 'width': 59, - 'capacity': { - 'Byte': { + "R11x59": { + "height": 11, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 248, + ErrorCorrectionLevel.H: 120, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 30, ErrorCorrectionLevel.H: 14, }, }, }, - 'R11x77': { - 'height': 11, - 'width': 77, - 'capacity': { - 'Byte': { + "R11x77": { + "height": 11, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 344, + ErrorCorrectionLevel.H: 184, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 41, ErrorCorrectionLevel.H: 21, }, }, }, - 'R11x99': { - 'height': 11, - 'width': 99, - 'capacity': { - 'Byte': { + "R11x99": { + "height": 11, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 456, + ErrorCorrectionLevel.H: 232, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 55, ErrorCorrectionLevel.H: 27, }, }, }, - 'R11x139': { - 'height': 11, - 'width': 139, - 'capacity': { - 'Byte': { + "R11x139": { + "height": 11, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 672, + ErrorCorrectionLevel.H: 336, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 82, ErrorCorrectionLevel.H: 40, }, }, }, - 'R13x27': { - 'height': 13, - 'width': 27, - 'capacity': { - 'Byte': { + "R13x27": { + "height": 13, + "width": 27, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 96, + ErrorCorrectionLevel.H: 56, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 11, ErrorCorrectionLevel.H: 6, }, }, }, - 'R13x43': { - 'height': 13, - 'width': 43, - 'capacity': { - 'Byte': { + "R13x43": { + "height": 13, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 216, + ErrorCorrectionLevel.H: 104, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 26, ErrorCorrectionLevel.H: 12, }, }, }, - 'R13x59': { - 'height': 13, - 'width': 59, - 'capacity': { - 'Byte': { + "R13x59": { + "height": 13, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 304, + ErrorCorrectionLevel.H: 160, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 36, ErrorCorrectionLevel.H: 18, }, }, }, - 'R13x77': { - 'height': 13, - 'width': 77, - 'capacity': { - 'Byte': { + "R13x77": { + "height": 13, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 424, + ErrorCorrectionLevel.H: 232, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 51, ErrorCorrectionLevel.H: 27, }, }, }, - 'R13x99': { - 'height': 13, - 'width': 99, - 'capacity': { - 'Byte': { + "R13x99": { + "height": 13, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 584, + ErrorCorrectionLevel.H: 280, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 71, ErrorCorrectionLevel.H: 33, }, }, }, - 'R13x139': { - 'height': 13, - 'width': 139, - 'capacity': { - 'Byte': { + "R13x139": { + "height": 13, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 848, + ErrorCorrectionLevel.H: 432, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 104, ErrorCorrectionLevel.H: 52, }, }, }, - 'R15x43': { - 'height': 15, - 'width': 43, - 'capacity': { - 'Byte': { + "R15x43": { + "height": 15, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 264, + ErrorCorrectionLevel.H: 120, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 31, ErrorCorrectionLevel.H: 13, }, }, }, - 'R15x59': { - 'height': 15, - 'width': 59, - 'capacity': { - 'Byte': { + "R15x59": { + "height": 15, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 384, + ErrorCorrectionLevel.H: 208, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 46, ErrorCorrectionLevel.H: 24, }, }, }, - 'R15x77': { - 'height': 15, - 'width': 77, - 'capacity': { - 'Byte': { + "R15x77": { + "height": 15, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 536, + ErrorCorrectionLevel.H: 248, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 65, ErrorCorrectionLevel.H: 29, }, }, }, - 'R15x99': { - 'height': 15, - 'width': 99, - 'capacity': { - 'Byte': { + "R15x99": { + "height": 15, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 704, + ErrorCorrectionLevel.H: 384, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 86, ErrorCorrectionLevel.H: 46, }, }, }, - 'R15x139': { - 'height': 15, - 'width': 139, - 'capacity': { - 'Byte': { + "R15x139": { + "height": 15, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 1016, + ErrorCorrectionLevel.H: 552, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 125, ErrorCorrectionLevel.H: 67, }, }, }, - 'R17x43': { - 'height': 17, - 'width': 43, - 'capacity': { - 'Byte': { + "R17x43": { + "height": 17, + "width": 43, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 312, + ErrorCorrectionLevel.H: 168, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 37, ErrorCorrectionLevel.H: 19, }, }, }, - 'R17x59': { - 'height': 17, - 'width': 59, - 'capacity': { - 'Byte': { + "R17x59": { + "height": 17, + "width": 59, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 448, + ErrorCorrectionLevel.H: 224, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 54, ErrorCorrectionLevel.H: 26, }, }, }, - 'R17x77': { - 'height': 17, - 'width': 77, - 'capacity': { - 'Byte': { + "R17x77": { + "height": 17, + "width": 77, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 624, + ErrorCorrectionLevel.H: 304, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 76, ErrorCorrectionLevel.H: 36, }, }, }, - 'R17x99': { - 'height': 17, - 'width': 99, - 'capacity': { - 'Byte': { + "R17x99": { + "height": 17, + "width": 99, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 800, + ErrorCorrectionLevel.H: 448, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 98, ErrorCorrectionLevel.H: 54, }, }, }, - 'R17x139': { - 'height': 17, - 'width': 139, - 'capacity': { - 'Byte': { + "R17x139": { + "height": 17, + "width": 139, + "number_of_data_bits": { + ErrorCorrectionLevel.M: 1216, + ErrorCorrectionLevel.H: 608, + }, + "capacity": { + "Byte": { ErrorCorrectionLevel.M: 150, ErrorCorrectionLevel.H: 74, }, }, }, -} \ No newline at end of file +} diff --git a/src/rmqrcode/format/error_correction_level.py b/src/rmqrcode/format/error_correction_level.py index 7483698..84e18e3 100644 --- a/src/rmqrcode/format/error_correction_level.py +++ b/src/rmqrcode/format/error_correction_level.py @@ -1,5 +1,6 @@ from enum import Enum + class ErrorCorrectionLevel(Enum): M = 0 - H = 1 \ No newline at end of file + H = 1 diff --git a/src/rmqrcode/format/mask.py b/src/rmqrcode/format/mask.py index 4822236..4ce92b7 100644 --- a/src/rmqrcode/format/mask.py +++ b/src/rmqrcode/format/mask.py @@ -1,2 +1,2 @@ def mask(x, y): - return (y//2 + x//3) % 2 == 0 + return (y // 2 + x // 3) % 2 == 0 diff --git a/src/rmqrcode/format/qr_versions.py b/src/rmqrcode/format/qr_versions.py deleted file mode 100644 index 1c29e8a..0000000 --- a/src/rmqrcode/format/qr_versions.py +++ /dev/null @@ -1,878 +0,0 @@ -from .error_correction_level import ErrorCorrectionLevel - - -qr_versions = { - 'R7x43': { - 'version_indicator': 0b00000, - 'height': 7, - 'width': 43, - 'remainder_bits': 0, - 'character_count_length': 3, - 'codewords_total': 13, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 13, - 'k': 6, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 13, - 'k': 3, - }, - ] - } - }, - 'R7x59': { - 'version_indicator': 0b00001, - 'height': 7, - 'width': 59, - 'remainder_bits': 3, - 'character_count_length': 4, - 'codewords_total': 21, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 21, - 'k': 12, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 21, - 'k': 7, - }, - ] - } - }, - 'R7x77': { - 'version_indicator': 0b00010, - 'height': 7, - 'width': 77, - 'remainder_bits': 5, - 'character_count_length': 5, - 'codewords_total': 32, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 32, - 'k': 20, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 32, - 'k': 10, - }, - ] - } - }, - 'R7x99': { - 'version_indicator': 0b00011, - 'height': 7, - 'width': 99, - 'remainder_bits': 6, - 'character_count_length': 5, - 'codewords_total': 44, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 44, - 'k': 28, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 44, - 'k': 14, - }, - ] - } - }, - 'R7x139': { - 'version_indicator': 0b00100, - 'height': 7, - 'width': 139, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 68, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 68, - 'k': 44, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 34, - 'k': 12, - }, - ] - } - }, - 'R9x43': { - 'version_indicator': 0b00101, - 'height': 9, - 'width': 43, - 'remainder_bits': 2, - 'character_count_length': 4, - 'codewords_total': 21, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 21, - 'k': 12, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 21, - 'k': 7, - }, - ] - } - }, - 'R9x59': { - 'version_indicator': 0b00110, - 'height': 9, - 'width': 59, - 'remainder_bits': 3, - 'character_count_length': 5, - 'codewords_total': 33, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 33, - 'k': 21, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 33, - 'k': 11, - }, - ] - } - }, - 'R9x77': { - 'version_indicator': 0b00111, - 'height': 9, - 'width': 77, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 49, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 49, - 'k': 31, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 24, - 'k': 8, - }, - { - 'num': 1, - 'c': 25, - 'k': 9, - }, - ] - } - }, - 'R9x99': { - 'version_indicator': 0b01000, - 'height': 9, - 'width': 99, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 66, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 66, - 'k': 42, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 33, - 'k': 11, - }, - ] - } - }, - 'R9x139': { - 'version_indicator': 0b01001, - 'height': 9, - 'width': 139, - 'remainder_bits': 5, - 'character_count_length': 6, - 'codewords_total': 99, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 49, - 'k': 31, - }, - { - 'num': 1, - 'c': 50, - 'k': 32, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 3, - 'c': 33, - 'k': 11, - } - ] - } - }, - 'R11x27': { - 'version_indicator': 0b01010, - 'height': 11, - 'width': 27, - 'remainder_bits': 2, - 'character_count_length': 3, - 'codewords_total': 15, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 15, - 'k': 7, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 15, - 'k': 5, - } - ] - } - }, - 'R11x43': { - 'version_indicator': 0b01011, - 'height': 11, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 31, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 31, - 'k': 19, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 31, - 'k': 11, - } - ] - } - }, - 'R11x59': { - 'version_indicator': 0b01100, - 'height': 11, - 'width': 59, - 'remainder_bits': 0, - 'character_count_length': 5, - 'codewords_total': 47, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 47, - 'k': 31, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 23, - 'k': 7, - }, - { - 'num': 1, - 'c': 24, - 'k': 8, - }, - ] - } - }, - 'R11x77': { - 'version_indicator': 0b01101, - 'height': 11, - 'width': 77, - 'remainder_bits': 2, - 'character_count_length': 6, - 'codewords_total': 67, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 67, - 'k': 43, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 33, - 'k': 11, - }, - { - 'num': 1, - 'c': 34, - 'k': 12, - }, - ] - } - }, - 'R11x99': { - 'version_indicator': 0b01110, - 'height': 11, - 'width': 99, - 'remainder_bits': 7, - 'character_count_length': 6, - 'codewords_total': 89, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 44, - 'k': 28, - }, - { - 'num': 1, - 'c': 45, - 'k': 29, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 44, - 'k': 14, - }, - { - 'num': 1, - 'c': 45, - 'k': 15, - }, - ] - } - }, - 'R11x139': { - 'version_indicator': 0b01111, - 'height': 11, - 'width': 139, - 'remainder_bits': 6, - 'character_count_length': 7, - 'codewords_total': 132, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 66, - 'k': 42, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 3, - 'c': 44, - 'k': 14, - } - ] - } - }, - 'R13x27': { - 'version_indicator': 0b10000, - 'height': 13, - 'width': 27, - 'character_count_length': 4, - 'remainder_bits': 4, - 'codewords_total': 21, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 21, - 'k': 14, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 21, - 'k': 7, - } - ] - } - }, - 'R13x43': { - 'version_indicator': 0b10001, - 'height': 13, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 5, - 'codewords_total': 41, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 41, - 'k': 27, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 41, - 'k': 13, - } - ] - } - }, - 'R13x59': { - 'version_indicator': 0b10010, - 'height': 13, - 'width': 59, - 'remainder_bits': 6, - 'character_count_length': 6, - 'codewords_total': 60, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 60, - 'k': 38, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 30, - 'k': 10, - } - ] - } - }, - 'R13x77': { - 'version_indicator': 0b10011, - 'height': 13, - 'width': 77, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 85, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 42, - 'k': 26, - }, - { - 'num': 1, - 'c': 43, - 'k': 27, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 42, - 'k': 14, - }, - { - 'num': 1, - 'c': 43, - 'k': 15, - }, - ] - } - }, - 'R13x99': { - 'version_indicator': 0b10100, - 'height': 13, - 'width': 99, - 'remainder_bits': 3, - 'character_count_length': 7, - 'codewords_total': 113, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 56, - 'k': 36, - }, - { - 'num': 1, - 'c': 57, - 'k': 37, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 37, - 'k': 11, - }, - { - 'num': 2, - 'c': 38, - 'k': 12, - }, - ] - } - }, - 'R13x139': { - 'version_indicator': 0b10101, - 'height': 13, - 'width': 139, - 'remainder_bits': 0, - 'character_count_length': 7, - 'codewords_total': 166, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 55, - 'k': 35, - }, - { - 'num': 1, - 'c': 56, - 'k': 36, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 41, - 'k': 13, - }, - { - 'num': 2, - 'c': 42, - 'k': 14, - }, - ] - } - }, - 'R15x43': { - 'version_indicator': 0b10110, - 'height': 15, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 51, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 51, - 'k': 33, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 25, - 'k': 7, - }, - { - 'num': 1, - 'c': 26, - 'k': 8, - }, - ] - } - }, - 'R15x59': { - 'version_indicator': 0b10111, - 'height': 15, - 'width': 59, - 'remainder_bits': 4, - 'character_count_length': 6, - 'codewords_total': 74, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 74, - 'k': 48, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 37, - 'k': 13, - } - ] - } - }, - 'R15x77': { - 'version_indicator': 0b11000, - 'height': 15, - 'width': 77, - 'remainder_bits': 6, - 'character_count_length': 7, - 'codewords_total': 103, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 51, - 'k': 33, - }, - { - 'num': 1, - 'c': 52, - 'k': 34, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 34, - 'k': 10, - }, - { - 'num': 1, - 'c': 35, - 'k': 11, - }, - ] - } - }, - 'R15x99': { - 'version_indicator': 0b11001, - 'height': 15, - 'width': 99, - 'remainder_bits': 7, - 'character_count_length': 7, - 'codewords_total': 136, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 68, - 'k': 44, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 4, - 'c': 34, - 'k': 12, - }, - ], - } - }, - 'R15x139': { - 'version_indicator': 0b11010, - 'height': 15, - 'width': 139, - 'remainder_bits': 2, - 'character_count_length': 7, - 'codewords_total': 199, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 66, - 'k': 42, - }, - { - 'num': 1, - 'c': 67, - 'k': 43, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 39, - 'k': 13, - }, - { - 'num': 4, - 'c': 40, - 'k': 14, - }, - ], - } - }, - 'R17x43': { - 'version_indicator': 0b11011, - 'height': 17, - 'width': 43, - 'remainder_bits': 1, - 'character_count_length': 6, - 'codewords_total': 61, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 1, - 'c': 60, - 'k': 39, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 30, - 'k': 10, - }, - { - 'num': 1, - 'c': 31, - 'k': 11, - }, - ] - } - }, - 'R17x59': { - 'version_indicator': 0b11100, - 'height': 17, - 'width': 59, - 'remainder_bits': 2, - 'character_count_length': 6, - 'codewords_total': 88, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 44, - 'k': 28, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 44, - 'k': 14, - } - ] - } - }, - 'R17x77': { - 'version_indicator': 0b11101, - 'height': 17, - 'width': 77, - 'remainder_bits': 0, - 'character_count_length': 7, - 'codewords_total': 122, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 61, - 'k': 39, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 1, - 'c': 40, - 'k': 12, - }, - { - 'num': 2, - 'c': 41, - 'k': 13, - }, - ], - } - }, - 'R17x99': { - 'version_indicator': 0b11110, - 'height': 17, - 'width': 99, - 'remainder_bits': 3, - 'character_count_length': 7, - 'codewords_total': 160, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 2, - 'c': 53, - 'k': 33, - }, - { - 'num': 1, - 'c': 54, - 'k': 34, - } - ], - ErrorCorrectionLevel.H: [ - { - 'num': 4, - 'c': 40, - 'k': 14, - }, - ], - } - }, - 'R17x139': { - 'version_indicator': 0b11111, - 'height': 17, - 'width': 139, - 'remainder_bits': 4, - 'character_count_length': 8, - 'codewords_total': 232, - 'blocks': { - ErrorCorrectionLevel.M: [ - { - 'num': 4, - 'c': 58, - 'k': 38, - }, - ], - ErrorCorrectionLevel.H: [ - { - 'num': 2, - 'c': 38, - 'k': 12, - }, - { - 'num': 4, - 'c': 39, - 'k': 13, - }, - ], - } - }, -} diff --git a/src/rmqrcode/format/rmqr_versions.py b/src/rmqrcode/format/rmqr_versions.py new file mode 100644 index 0000000..c35c472 --- /dev/null +++ b/src/rmqrcode/format/rmqr_versions.py @@ -0,0 +1,1038 @@ +from ..encoder import AlphanumericEncoder, ByteEncoder, KanjiEncoder, NumericEncoder +from .error_correction_level import ErrorCorrectionLevel + +rMQRVersions = { + "R7x43": { + "version_indicator": 0b00000, + "height": 7, + "width": 43, + "remainder_bits": 0, + "character_count_indicator_length": { + NumericEncoder: 4, + AlphanumericEncoder: 3, + ByteEncoder: 3, + KanjiEncoder: 2, + }, + "codewords_total": 13, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 13, + "k": 6, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 13, + "k": 3, + }, + ], + }, + }, + "R7x59": { + "version_indicator": 0b00001, + "height": 7, + "width": 59, + "remainder_bits": 3, + "character_count_indicator_length": { + NumericEncoder: 5, + AlphanumericEncoder: 5, + ByteEncoder: 4, + KanjiEncoder: 3, + }, + "codewords_total": 21, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 21, + "k": 12, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 21, + "k": 7, + }, + ], + }, + }, + "R7x77": { + "version_indicator": 0b00010, + "height": 7, + "width": 77, + "remainder_bits": 5, + "character_count_indicator_length": { + NumericEncoder: 6, + AlphanumericEncoder: 5, + ByteEncoder: 5, + KanjiEncoder: 4, + }, + "codewords_total": 32, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 32, + "k": 20, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 32, + "k": 10, + }, + ], + }, + }, + "R7x99": { + "version_indicator": 0b00011, + "height": 7, + "width": 99, + "remainder_bits": 6, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 5, + KanjiEncoder: 5, + }, + "codewords_total": 44, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 44, + "k": 28, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 44, + "k": 14, + }, + ], + }, + }, + "R7x139": { + "version_indicator": 0b00100, + "height": 7, + "width": 139, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 68, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 68, + "k": 44, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 34, + "k": 12, + }, + ], + }, + }, + "R9x43": { + "version_indicator": 0b00101, + "height": 9, + "width": 43, + "remainder_bits": 2, + "character_count_indicator_length": { + NumericEncoder: 5, + AlphanumericEncoder: 5, + ByteEncoder: 4, + KanjiEncoder: 3, + }, + "codewords_total": 21, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 21, + "k": 12, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 21, + "k": 7, + }, + ], + }, + }, + "R9x59": { + "version_indicator": 0b00110, + "height": 9, + "width": 59, + "remainder_bits": 3, + "character_count_indicator_length": { + NumericEncoder: 6, + AlphanumericEncoder: 5, + ByteEncoder: 5, + KanjiEncoder: 4, + }, + "codewords_total": 33, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 33, + "k": 21, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 33, + "k": 11, + }, + ], + }, + }, + "R9x77": { + "version_indicator": 0b00111, + "height": 9, + "width": 77, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 5, + KanjiEncoder: 5, + }, + "codewords_total": 49, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 49, + "k": 31, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 24, + "k": 8, + }, + { + "num": 1, + "c": 25, + "k": 9, + }, + ], + }, + }, + "R9x99": { + "version_indicator": 0b01000, + "height": 9, + "width": 99, + "remainder_bits": 4, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 66, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 66, + "k": 42, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 33, + "k": 11, + }, + ], + }, + }, + "R9x139": { + "version_indicator": 0b01001, + "height": 9, + "width": 139, + "remainder_bits": 5, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 6, + KanjiEncoder: 6, + }, + "codewords_total": 99, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 49, + "k": 31, + }, + { + "num": 1, + "c": 50, + "k": 32, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 3, + "c": 33, + "k": 11, + } + ], + }, + }, + "R11x27": { + "version_indicator": 0b01010, + "height": 11, + "width": 27, + "remainder_bits": 2, + "character_count_indicator_length": { + NumericEncoder: 4, + AlphanumericEncoder: 4, + ByteEncoder: 3, + KanjiEncoder: 2, + }, + "codewords_total": 15, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 15, + "k": 7, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 15, + "k": 5, + } + ], + }, + }, + "R11x43": { + "version_indicator": 0b01011, + "height": 11, + "width": 43, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 6, + AlphanumericEncoder: 5, + ByteEncoder: 5, + KanjiEncoder: 4, + }, + "codewords_total": 31, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 31, + "k": 19, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 31, + "k": 11, + } + ], + }, + }, + "R11x59": { + "version_indicator": 0b01100, + "height": 11, + "width": 59, + "remainder_bits": 0, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 5, + KanjiEncoder: 5, + }, + "codewords_total": 47, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 47, + "k": 31, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 23, + "k": 7, + }, + { + "num": 1, + "c": 24, + "k": 8, + }, + ], + }, + }, + "R11x77": { + "version_indicator": 0b01101, + "height": 11, + "width": 77, + "remainder_bits": 2, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 67, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 67, + "k": 43, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 33, + "k": 11, + }, + { + "num": 1, + "c": 34, + "k": 12, + }, + ], + }, + }, + "R11x99": { + "version_indicator": 0b01110, + "height": 11, + "width": 99, + "remainder_bits": 7, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 6, + KanjiEncoder: 6, + }, + "codewords_total": 89, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 44, + "k": 28, + }, + { + "num": 1, + "c": 45, + "k": 29, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 44, + "k": 14, + }, + { + "num": 1, + "c": 45, + "k": 15, + }, + ], + }, + }, + "R11x139": { + "version_indicator": 0b01111, + "height": 11, + "width": 139, + "remainder_bits": 6, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 132, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 66, + "k": 42, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 3, + "c": 44, + "k": 14, + } + ], + }, + }, + "R13x27": { + "version_indicator": 0b10000, + "height": 13, + "width": 27, + "character_count_indicator_length": { + NumericEncoder: 5, + AlphanumericEncoder: 5, + ByteEncoder: 4, + KanjiEncoder: 3, + }, + "remainder_bits": 4, + "codewords_total": 21, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 21, + "k": 14, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 21, + "k": 7, + } + ], + }, + }, + "R13x43": { + "version_indicator": 0b10001, + "height": 13, + "width": 43, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 6, + AlphanumericEncoder: 6, + ByteEncoder: 5, + KanjiEncoder: 5, + }, + "codewords_total": 41, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 41, + "k": 27, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 41, + "k": 13, + } + ], + }, + }, + "R13x59": { + "version_indicator": 0b10010, + "height": 13, + "width": 59, + "remainder_bits": 6, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 60, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 60, + "k": 38, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 30, + "k": 10, + } + ], + }, + }, + "R13x77": { + "version_indicator": 0b10011, + "height": 13, + "width": 77, + "remainder_bits": 4, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 7, + ByteEncoder: 6, + KanjiEncoder: 6, + }, + "codewords_total": 85, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 42, + "k": 26, + }, + { + "num": 1, + "c": 43, + "k": 27, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 42, + "k": 14, + }, + { + "num": 1, + "c": 43, + "k": 15, + }, + ], + }, + }, + "R13x99": { + "version_indicator": 0b10100, + "height": 13, + "width": 99, + "remainder_bits": 3, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 113, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 56, + "k": 36, + }, + { + "num": 1, + "c": 57, + "k": 37, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 37, + "k": 11, + }, + { + "num": 2, + "c": 38, + "k": 12, + }, + ], + }, + }, + "R13x139": { + "version_indicator": 0b10101, + "height": 13, + "width": 139, + "remainder_bits": 0, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 8, + ByteEncoder: 7, + KanjiEncoder: 7, + }, + "codewords_total": 166, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 55, + "k": 35, + }, + { + "num": 1, + "c": 56, + "k": 36, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 41, + "k": 13, + }, + { + "num": 2, + "c": 42, + "k": 14, + }, + ], + }, + }, + "R15x43": { + "version_indicator": 0b10110, + "height": 15, + "width": 43, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 51, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 51, + "k": 33, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 25, + "k": 7, + }, + { + "num": 1, + "c": 26, + "k": 8, + }, + ], + }, + }, + "R15x59": { + "version_indicator": 0b10111, + "height": 15, + "width": 59, + "remainder_bits": 4, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 7, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 74, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 74, + "k": 48, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 37, + "k": 13, + } + ], + }, + }, + "R15x77": { + "version_indicator": 0b11000, + "height": 15, + "width": 77, + "remainder_bits": 6, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 103, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 51, + "k": 33, + }, + { + "num": 1, + "c": 52, + "k": 34, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 34, + "k": 10, + }, + { + "num": 1, + "c": 35, + "k": 11, + }, + ], + }, + }, + "R15x99": { + "version_indicator": 0b11001, + "height": 15, + "width": 99, + "remainder_bits": 7, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 136, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 68, + "k": 44, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 4, + "c": 34, + "k": 12, + }, + ], + }, + }, + "R15x139": { + "version_indicator": 0b11010, + "height": 15, + "width": 139, + "remainder_bits": 2, + "character_count_indicator_length": { + NumericEncoder: 9, + AlphanumericEncoder: 8, + ByteEncoder: 7, + KanjiEncoder: 7, + }, + "codewords_total": 199, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 66, + "k": 42, + }, + { + "num": 1, + "c": 67, + "k": 43, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 39, + "k": 13, + }, + { + "num": 4, + "c": 40, + "k": 14, + }, + ], + }, + }, + "R17x43": { + "version_indicator": 0b11011, + "height": 17, + "width": 43, + "remainder_bits": 1, + "character_count_indicator_length": { + NumericEncoder: 7, + AlphanumericEncoder: 6, + ByteEncoder: 6, + KanjiEncoder: 5, + }, + "codewords_total": 61, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 1, + "c": 60, + "k": 39, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 30, + "k": 10, + }, + { + "num": 1, + "c": 31, + "k": 11, + }, + ], + }, + }, + "R17x59": { + "version_indicator": 0b11100, + "height": 17, + "width": 59, + "remainder_bits": 2, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 6, + KanjiEncoder: 6, + }, + "codewords_total": 88, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 44, + "k": 28, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 44, + "k": 14, + } + ], + }, + }, + "R17x77": { + "version_indicator": 0b11101, + "height": 17, + "width": 77, + "remainder_bits": 0, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 7, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 122, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 61, + "k": 39, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 1, + "c": 40, + "k": 12, + }, + { + "num": 2, + "c": 41, + "k": 13, + }, + ], + }, + }, + "R17x99": { + "version_indicator": 0b11110, + "height": 17, + "width": 99, + "remainder_bits": 3, + "character_count_indicator_length": { + NumericEncoder: 8, + AlphanumericEncoder: 8, + ByteEncoder: 7, + KanjiEncoder: 6, + }, + "codewords_total": 160, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 2, + "c": 53, + "k": 33, + }, + { + "num": 1, + "c": 54, + "k": 34, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 4, + "c": 40, + "k": 14, + }, + ], + }, + }, + "R17x139": { + "version_indicator": 0b11111, + "height": 17, + "width": 139, + "remainder_bits": 4, + "character_count_indicator_length": { + NumericEncoder: 9, + AlphanumericEncoder: 8, + ByteEncoder: 8, + KanjiEncoder: 7, + }, + "codewords_total": 232, + "blocks": { + ErrorCorrectionLevel.M: [ + { + "num": 4, + "c": 58, + "k": 38, + }, + ], + ErrorCorrectionLevel.H: [ + { + "num": 2, + "c": 38, + "k": 12, + }, + { + "num": 4, + "c": 39, + "k": 13, + }, + ], + }, + }, +} diff --git a/src/rmqrcode/qr_image.py b/src/rmqrcode/qr_image.py index 72fad4a..a2ee637 100644 --- a/src/rmqrcode/qr_image.py +++ b/src/rmqrcode/qr_image.py @@ -1,29 +1,39 @@ -from .enums.color import Color - -from PIL import Image +from PIL import Image, ImageDraw class QRImage: - def __init__(self, qr): - self._img = Image.new('RGB', (qr.width() + 4, qr.height() + 4), (255, 255, 255)) - self._make_image(qr) - + def __init__(self, qr, module_size=10): + self._module_size = module_size + qr_list = qr.to_list() + self._img = Image.new("RGB", (len(qr_list[0]) * module_size, len(qr_list) * module_size), (255, 255, 255)) + self._make_image(qr_list) def show(self): self._img.show() pass + def get_ndarray(self): + try: + import numpy as np + except ImportError: + raise ImportError("numpy is not installed") + + return np.array(self._img) def save(self, name): self._img.save(name) - - def _make_image(self, qr): - for y in range(qr.height()): - for x in range(qr.width()): - r, g, b = 125, 125, 125 - if qr.value_at(x, y) == Color.BLACK: - r, g, b = 0, 0, 0 - elif qr.value_at(x, y) == Color.WHITE: - r, g, b, = 255, 255, 255 - self._img.putpixel((x+2, y+2), (r, g, b)) \ No newline at end of file + def _make_image(self, qr_list): + draw = ImageDraw.Draw(self._img) + for y in range(len(qr_list)): + for x in range(len(qr_list[0])): + r, g, b = (0, 0, 0) if qr_list[y][x] else (255, 255, 255) + draw.rectangle( + xy=( + x * self._module_size, + y * self._module_size, + (x + 1) * self._module_size, + (y + 1) * self._module_size, + ), + fill=(r, g, b), + ) diff --git a/src/rmqrcode/rmqrcode.py b/src/rmqrcode/rmqrcode.py index 87ab1c2..8330a8a 100644 --- a/src/rmqrcode/rmqrcode.py +++ b/src/rmqrcode/rmqrcode.py @@ -1,117 +1,348 @@ -from .format.error_correction_level import ErrorCorrectionLevel -from .format.qr_versions import qr_versions -from .format.data_capacities import data_capacities +"""A module to make an rMQR Code. + +Example: + Use the rMQR.fit method to make an rMQR automatically with some options. + + qr = rMQR.fit( + "https://oudon.xyz", + ecc=ErrorCorrectionLevel.M, + fit_strategy=FitStrategy.MINIMIZE_WIDTH + ) + + The following example shows how to select the size of an rMQR Code. + + qr = rMQR("R11x139", ErrorCorrectionLevel.H) + qr.make("https://oudon.xyz") + +""" + +import logging + +from . 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 from .format.alignment_pattern_coordinates import AlignmentPatternCoordinates +from .format.data_capacities import DataCapacities +from .format.error_correction_level import ErrorCorrectionLevel from .format.generator_polynomials import GeneratorPolynomials from .format.mask import mask - -from .encoder.byte_encoder import ByteEncoder +from .format.rmqr_versions import rMQRVersions from .util.error_correction import compute_bch, compute_reed_solomon from .util.utilities import split_into_8bits -from .enums.color import Color -from .enums.fit_strategy import FitStrategy -import logging class rMQR: + """A class to make an rMQR Code. + + Attributes: + QUIET_ZONE_MODULES (int): The width of the quiet zone. + + """ + + QUIET_ZONE_MODULES = 2 + @staticmethod def _init_logger(): + """Initializes a logger and returns it. + + Returns: + logging.RootLogger: Logger + + """ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) logger.setLevel(logging.DEBUG) logger.propagate = True return logger - @staticmethod - def fit(data,ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): + def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED): + """Attempts to make an rMQR have optimized version for given data. + + Args: + data (str): Data string to encode. + ecc (rmqrcode.ErrorCorrectionLevel): Error correction level. + fit_strategy (rmqrcode.FitStrategy): Strategy how determine rMQR Code version. + + Returns: + rmqrcode.rMQR: Optimized rMQR Code. + + Raises: + rmqrcode.DataTooLongError: If the data is too long to encode. + + """ logger = rMQR._init_logger() - data_length = ByteEncoder.length(data) ok_versions = [] determined_width = set() determined_height = set() logger.debug("Select rMQR Code version") - for version_name, qr_version in data_capacities.items(): - if data_length <= qr_version['capacity']['Byte'][ecc]: - width, height = qr_version['width'], qr_version['height'] - if not width in determined_width and not height in determined_height: - determined_width.add(width) - determined_height.add(height) - ok_versions.append({ - 'version': version_name, - 'width': width, - 'height': height, - }) - logger.debug(f"ok: {version_name}") + for version_name, qr_version in DataCapacities.items(): + optimizer = qr_segments.SegmentOptimizer() + try: + optimized_segments = optimizer.compute(data, version_name, ecc) + except DataTooLongError: + continue + + width, height = qr_version["width"], qr_version["height"] + if width not in determined_width and height not in determined_height: + determined_width.add(width) + determined_height.add(height) + ok_versions.append( + { + "version": version_name, + "width": width, + "height": height, + "segments": optimized_segments, + } + ) + logger.debug(f"ok: {version_name}") if len(ok_versions) == 0: raise DataTooLongError("The data is too long.") if fit_strategy == FitStrategy.MINIMIZE_WIDTH: - sort_key = lambda x: x['width'] + + def sort_key(x): + return x["width"] + elif fit_strategy == FitStrategy.MINIMIZE_HEIGHT: - sort_key = lambda x: x['height'] + + def sort_key(x): + return x["height"] + elif fit_strategy == FitStrategy.BALANCED: - sort_key = lambda x: x['height'] * 9 + x['width'] + + def sort_key(x): + return x["height"] * 9 + x["width"] + selected = sorted(ok_versions, key=sort_key)[0] logger.debug(f"selected: {selected}") - qr = rMQR(selected['version'], ecc) - qr.make(data) + qr = rMQR(selected["version"], ecc) + qr.add_segments(selected["segments"]) + qr.make() return qr + def _optimized_segments(self, data): + optimizer = qr_segments.SegmentOptimizer() + return optimizer.compute(data, self.version_name(), self._error_correction_level) - def __init__(self, version, ecc, logger=None): + def __init__(self, version, ecc, with_quiet_zone=True, logger=None): self._logger = logger or rMQR._init_logger() if not rMQR.validate_version(version): raise IllegalVersionError("The rMQR version is illegal.") - qr_version = qr_versions[version] + qr_version = rMQRVersions[version] self._version = version - self._height = qr_version['height'] - self._width = qr_version['width'] + self._height = qr_version["height"] + self._width = qr_version["width"] self._error_correction_level = ecc self._qr = [[Color.UNDEFINED for x in range(self._width)] for y in range(self._height)] + 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 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. + + 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() + try: + encoded_data = self._encode_data() + except DataTooLongError: + raise DataTooLongError() - def make(self, data): self._put_finder_pattern() self._put_corner_finder_pattern() self._put_alignment_pattern() self._put_timing_pattern() self._put_version_information() - mask_area = self._put_data(data); + mask_area = self._put_data(encoded_data) 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()] + 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, data_bits_max) + + if len(res) > data_bits_max: + raise DataTooLongError("The data is too long.") + + return res + + 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 + 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. + data_bits_max: The max length of data bits. + + Returns: + str: The string after appending the terminator. + + """ + if len(data) + 3 <= data_bits_max: + data += "000" + return data def version_name(self): - return f"R{self._height}x{self._width}" + """Returns the version name. + Returns: + str: The version name. + + Examples: + >>> qr.version_name() + "R13x77" + + """ + return f"R{self._height}x{self._width}" def size(self): - return (self.width(), self.height()) + """Returns the size. + + Returns: + tuple: The rMQR Code size. + Examples: + >>> qr.size() + (77, 13) + + Note: + This not includes the quiet zone. + + """ + return (self.width(), self.height()) def height(self): - return self._height + """Returns the height. + Returns: + int: The height. + + Note: + This not includes the quiet zone. + + """ + return self._height def width(self): - return self._width + """Returns the width. + + Returns: + int: The width. + Note: + This not includes the quiet zone. + + """ + return self._width def value_at(self, x, y): + """DEPRECATED: Returns the color at the point of (x, y). + + Returns: + rmqrcode.Color: The color of rMQRCode at the point of (x, y). + + Note: + This method is deprecated. Use to_list() alternatively. + This not includes the quiet zone. + + """ return self._qr[y][x] + def to_list(self, with_quiet_zone=True): + """Converts to two-dimensional list and returns it. - def to_list(self): - return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] + The value is 1 for the dark module and 0 for the light module. + + Args: + with_quiet_zone (bool): Flag to select whether include the quiet zone. + + Returns: + list: Converted list. + + """ + res = [] + if with_quiet_zone: + for y in range(self.QUIET_ZONE_MODULES): + res.append([0] * (self.width() + self.QUIET_ZONE_MODULES * 2)) + for row in self._to_binary_list(): + res.append([0] * self.QUIET_ZONE_MODULES + row + [0] * self.QUIET_ZONE_MODULES) + for y in range(self.QUIET_ZONE_MODULES): + res.append([0] * (self.width() + self.QUIET_ZONE_MODULES * 2)) + else: + res = self._to_binary_list() + return res + + def _to_binary_list(self): + """Converts to two-dimensional list and returns it. + + The value is 1 for the dark module and 0 for the light module. + Args: + with_quiet_zone (bool): Flag to select whether include the quiet zone. - def __str__(self): + Returns: + list: Converted list. + + Note: + This not includes the quiet zone. + """ + return [list(map(lambda x: 1 if x == Color.BLACK else 0, column)) for column in self._qr] + + def __str__(self, with_quiet_zone=True): res = "" show = {} @@ -122,19 +353,30 @@ def __str__(self): show[False] = "_" res += f"rMQR Version R{self._height}x{self._width}:\n" - for i in range(self._height): - for j in range(self._width): + if with_quiet_zone: + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + "\n") * self.QUIET_ZONE_MODULES + + for i in range(self.height()): + if with_quiet_zone: + res += show[False] * self.QUIET_ZONE_MODULES + + for j in range(self.width()): if self._qr[i][j] in show: res += show[self._qr[i][j]] else: res += self._qr[i][j] + + if with_quiet_zone: + res += show[False] * self.QUIET_ZONE_MODULES res += "\n" - return res + if with_quiet_zone: + res += (show[False] * (self.width() + self.QUIET_ZONE_MODULES * 2) + "\n") * self.QUIET_ZONE_MODULES + return res def _put_finder_pattern(self): # Finder pattern - # 周囲 + # Outer square for i in range(7): for j in range(7): if i == 0 or i == 6 or j == 0 or j == 6: @@ -142,10 +384,10 @@ def _put_finder_pattern(self): else: self._qr[i][j] = Color.WHITE - # 真ん中 + # Inner square for i in range(3): for j in range(3): - self._qr[2+i][2+j] = Color.BLACK + self._qr[2 + i][2 + j] = Color.BLACK # Separator for n in range(8): @@ -156,33 +398,31 @@ def _put_finder_pattern(self): self._qr[7][n] = Color.WHITE # Finder sub pattern - # 周囲 + # Outer square for i in range(5): for j in range(5): color = Color.BLACK if i == 0 or i == 4 or j == 0 or j == 4 else Color.WHITE - self._qr[self._height-i-1][self._width-j-1] = color - - # 真ん中 - self._qr[self._height-1-2][self._width-1-2] = Color.BLACK + self._qr[self._height - i - 1][self._width - j - 1] = color + # Inner square + self._qr[self._height - 1 - 2][self._width - 1 - 2] = Color.BLACK def _put_corner_finder_pattern(self): # Corner finder pattern - # 左下 - self._qr[self._height-1][0] = Color.BLACK - self._qr[self._height-1][1] = Color.BLACK - self._qr[self._height-1][2] = Color.BLACK + # Bottom left + self._qr[self._height - 1][0] = Color.BLACK + self._qr[self._height - 1][1] = Color.BLACK + self._qr[self._height - 1][2] = Color.BLACK if self._height >= 11: - self._qr[self._height-2][0] = Color.BLACK - self._qr[self._height-2][1] = Color.WHITE - - # 右上 - self._qr[0][self._width-1] = Color.BLACK - self._qr[0][self._width-2] = Color.BLACK - self._qr[1][self._width-1] = Color.BLACK - self._qr[1][self._width-2] = Color.WHITE + self._qr[self._height - 2][0] = Color.BLACK + self._qr[self._height - 2][1] = Color.WHITE + # Top right + self._qr[0][self._width - 1] = Color.BLACK + self._qr[0][self._width - 2] = Color.BLACK + self._qr[1][self._width - 1] = Color.BLACK + self._qr[1][self._width - 2] = Color.WHITE def _put_alignment_pattern(self): # Alignment pattern @@ -191,22 +431,21 @@ def _put_alignment_pattern(self): for i in range(3): for j in range(3): color = Color.BLACK if i == 0 or i == 2 or j == 0 or j == 2 else Color.WHITE - # 上側 + # Top side self._qr[i][center_x + j - 1] = color - # 下側 - self._qr[self._height-1-i][center_x + j - 1] = color - + # Bottom side + self._qr[self._height - 1 - i][center_x + j - 1] = color def _put_timing_pattern(self): # Timing pattern - # 横 + # Horizontal for j in range(self._width): color = Color.BLACK if (j + 1) % 2 else Color.WHITE for i in [0, self._height - 1]: if self._qr[i][j] == Color.UNDEFINED: self._qr[i][j] = color - # 縦 + # Vertical center_xs = [0, self._width - 1] center_xs.extend(AlignmentPatternCoordinates[self._width]) for i in range(self._height): @@ -215,13 +454,11 @@ def _put_timing_pattern(self): if self._qr[i][j] == Color.UNDEFINED: self._qr[i][j] = color - def _put_version_information(self): version_information = self._compute_version_info() self._put_version_information_finder_pattern_side(version_information) self._put_version_information_finder_sub_pattern_side(version_information) - def _put_version_information_finder_pattern_side(self, version_information): mask = 0b011111101010110010 version_information ^= mask @@ -230,8 +467,7 @@ def _put_version_information_finder_pattern_side(self, version_information): for n in range(18): di = n % 5 dj = n // 5 - self._qr[si+di][sj+dj] = Color.BLACK if version_information>>n & 1 else Color.WHITE - + self._qr[si + di][sj + dj] = Color.BLACK if version_information >> n & 1 else Color.WHITE def _put_version_information_finder_sub_pattern_side(self, version_information): mask = 0b100000101001111011 @@ -241,31 +477,47 @@ def _put_version_information_finder_sub_pattern_side(self, version_information): for n in range(15): di = n % 5 dj = n // 5 - self._qr[si+di][sj+dj] = Color.BLACK if version_information>>n & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-4] = Color.BLACK if version_information>>15 & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-3] = Color.BLACK if version_information>>16 & 1 else Color.WHITE - self._qr[self._height-1-5][self._width-1-2] = Color.BLACK if version_information>>17 & 1 else Color.WHITE - + self._qr[si + di][sj + dj] = Color.BLACK if version_information >> n & 1 else Color.WHITE + self._qr[self._height - 1 - 5][self._width - 1 - 4] = ( + Color.BLACK if version_information >> 15 & 1 else Color.WHITE + ) + self._qr[self._height - 1 - 5][self._width - 1 - 3] = ( + Color.BLACK if version_information >> 16 & 1 else Color.WHITE + ) + self._qr[self._height - 1 - 5][self._width - 1 - 2] = ( + Color.BLACK if version_information >> 17 & 1 else Color.WHITE + ) def _compute_version_info(self): - qr_version = qr_versions[self.version_name()] - version_information_data = qr_version['version_indicator'] + qr_version = rMQRVersions[self.version_name()] + version_information_data = qr_version["version_indicator"] if self._error_correction_level == ErrorCorrectionLevel.H: - version_information_data |= 1<<6 + version_information_data |= 1 << 6 reminder_polynomial = compute_bch(version_information_data) - version_information_data = version_information_data<<12 | reminder_polynomial + version_information_data = version_information_data << 12 | reminder_polynomial return version_information_data + def _put_data(self, encoded_data): + """Symbol character placement. + + 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: + encoded_data (str): The data after encoding. Expected all segments are joined. - def _put_data(self, data): - qr_version = qr_versions[self.version_name()] + Returns: + list: A two-dimensional list shows where encoding region. - character_count_length = qr_version['character_count_length'] - codewords_total = qr_version['codewords_total'] - encoded_data = self._convert_to_bites_data(data, character_count_length, codewords_total) + """ codewords = split_into_8bits(encoded_data) - # codeword数に満たない場合は規定の文字列を付与する + # Add the remainder codewords + qr_version = rMQRVersions[self.version_name()] + codewords_total = qr_version["codewords_total"] while True: if len(codewords) >= codewords_total: break @@ -275,11 +527,10 @@ def _put_data(self, data): codewords.append("00010001") data_codewords_per_block, rs_codewords_per_block = self._split_into_blocks( - codewords, - qr_version['blocks'][self._error_correction_level] + codewords, qr_version["blocks"][self._error_correction_level] ) - # データの並び替え + # Construct the final message codeword sequence # Data codewords final_codewords = [] for i in range(len(data_codewords_per_block[-1])): @@ -297,41 +548,43 @@ def _put_data(self, data): final_codewords.append(rs_codewords[i]) self._logger.debug(f"Put RS data codewords {i} : {rs_codewords[i]}") - # 配置 - dy = -1 # 最初は上方向 + # Codeword placement + dy = -1 # Up current_codeword_idx = 0 current_bit_idx = 0 cx, cy = self._width - 2, self._height - 6 - remainder_bits = qr_version['remainder_bits'] + remainder_bits = qr_version["remainder_bits"] mask_area = [[False for i in range(self._width)] for j in range(self._height)] while True: - for x in [cx, cx-1]: + for x in [cx, cx - 1]: if self._qr[cy][x] == Color.UNDEFINED: - # 空白のセルのみ処理する + # Process only empty cell if current_codeword_idx == len(final_codewords): - # codewordsを配置しきった場合はremainder_bitsがあれば配置する + # Remainder bits self._qr[cy][x] = Color.WHITE mask_area[cy][x] = True remainder_bits -= 1 else: - # codewordsを配置する - self._qr[cy][x] = Color.BLACK if final_codewords[current_codeword_idx][current_bit_idx] == '1' else Color.WHITE + # Codewords + self._qr[cy][x] = ( + Color.BLACK + if final_codewords[current_codeword_idx][current_bit_idx] == "1" + else Color.WHITE + ) mask_area[cy][x] = True current_bit_idx += 1 if current_bit_idx == 8: current_bit_idx = 0 current_codeword_idx += 1 - # codewordsの配置が終わりremainder_bitsも残っていなければ終了 if current_codeword_idx == len(final_codewords) and remainder_bits == 0: break - # codewordsの配置が終わりremainder_bitsも残っていなければ終了 if current_codeword_idx == len(final_codewords) and remainder_bits == 0: break - # 座標の更新 + # Update current coordinates if dy < 0 and cy == 1: cx -= 2 dy = 1 @@ -343,15 +596,14 @@ def _put_data(self, data): return mask_area - def _split_into_blocks(self, codewords, blocks_definition): data_idx, error_idx = 0, 0 data_codewords_per_block = [] rs_codewords_per_block = [] for block_definition in blocks_definition: - for i in range(block_definition['num']): - data_codewords_num = block_definition['k'] - rs_codewords_num = block_definition['c'] - block_definition['k'] + for i in range(block_definition["num"]): + data_codewords_num = block_definition["k"] + rs_codewords_num = block_definition["c"] - block_definition["k"] g = GeneratorPolynomials[rs_codewords_num] codewords_in_block = codewords[data_idx : data_idx + data_codewords_num] @@ -365,18 +617,19 @@ def _split_into_blocks(self, codewords, blocks_definition): return data_codewords_per_block, rs_codewords_per_block + def _apply_mask(self, mask_area): + """Data masking. - def _convert_to_bites_data(self, data, character_count_length, codewords_total): - encoded_data = ByteEncoder.encode(data, character_count_length) - - # 付加できるなら終端文字を付け加える - if len(encoded_data) + 3 <= codewords_total * 8: - encoded_data += "000" + This method applies the data mask. - return encoded_data + Args: + mask_area (list): A two-dimensional list shows where encoding region. + This is computed by self._put_data(). + Returns: + void - def _apply_mask(self, mask_area): + """ for y in range(self._height): for x in range(self._width): if not mask_area[y][x]: @@ -387,15 +640,25 @@ def _apply_mask(self, mask_area): elif self._qr[y][x] == Color.WHITE: self._qr[y][x] = Color.BLACK - @staticmethod def validate_version(version_name): - return version_name in qr_versions + """Check if the given version_name is valid + + Args: + version_name (str): Version name. + + Returns: + bool: Validation result. + Example: + >>> rMQR.validate_version("R13x77") + True -class DataTooLongError(ValueError): - pass + >>> rMQR.validate_version("R14x55") + False + >>> rMQR.validate_version("13, 77") + False -class IllegalVersionError(ValueError): - pass \ No newline at end of file + """ + return version_name in rMQRVersions diff --git a/src/rmqrcode/segments.py b/src/rmqrcode/segments.py new file mode 100644 index 0000000..02aa436 --- /dev/null +++ b/src/rmqrcode/segments.py @@ -0,0 +1,237 @@ +from . import encoder +from .errors import DataTooLongError +from .format.data_capacities import DataCapacities +from .format.rmqr_versions import rMQRVersions + +encoders = [ + encoder.NumericEncoder, + encoder.AlphanumericEncoder, + encoder.ByteEncoder, + encoder.KanjiEncoder, +] + + +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 + + 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, ecc): + """Computes the optimize segmentation for the given data. + + Args: + data (str): The data to encode. + version (str): The version name. + ecc (rmqrcode.ErrorCorrectionLevel): The error correction level. + + Returns: + list: The list of segments. + + Raises: + rmqrcode.DataTooLongError: If the data is too long to encode. + + """ + if len(data) > self.MAX_CHARACTER: + raise DataTooLongError() + + self.qr_version = rMQRVersions[version] + self._compute_costs(data) + best = self._find_best(data) + if best["cost"] > DataCapacities[version]["number_of_data_bits"][ecc]: + raise DataTooLongError + + path = self._reconstruct_path(best["index"]) + segments = self._compute_segments(path, data) + return segments + + def _compute_costs(self, data): + """Computes costs by dynamic programming. + + 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. + + 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, 0) + + for n in range(0, len(data)): + for mode in range(4): + for unfilled_length in range(3): + if self.dp[n][mode][unfilled_length] == self.INF: + continue + + for new_mode in range(4): + if not encoders[new_mode].is_valid_characters(data[n]): + continue + + if new_mode == mode: + cost, new_length = self._compute_new_state_without_mode_changing( + data[n], new_mode, unfilled_length + ) + else: + cost, new_length = self._compute_new_state_with_mode_changing( + data[n], new_mode, unfilled_length + ) + + if self.dp[n][mode][unfilled_length] + cost < self.dp[n + 1][new_mode][new_length]: + self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost + self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length) + + def _compute_new_state_without_mode_changing(self, character, new_mode, unfilled_length): + """Computes the new state values without mode changing. + + Args: + character (str): The current character. Assume this as one length string. + new_mode (int): The state of the new mode. + unfilled_length (int): The state of the current unfilled_length. + + Returns: + tuple: (cost, new_length). + + """ + encoder_class = encoders[new_mode] + if encoder_class == encoder.NumericEncoder: + new_length = (unfilled_length + 1) % 3 + cost = 4 if unfilled_length == 0 else 3 + elif encoder_class == encoder.AlphanumericEncoder: + new_length = (unfilled_length + 1) % 2 + cost = 6 if unfilled_length == 0 else 5 + elif encoder_class == encoder.ByteEncoder: + new_length = 0 + cost = 8 * len(character.encode("utf-8")) + elif encoder_class == encoder.KanjiEncoder: + new_length = 0 + cost = 13 + return (cost, new_length) + + def _compute_new_state_with_mode_changing(self, character, new_mode, unfilled_length): + """Computes the new state values with mode changing. + + Args: + character (str): The current character. Assume this as one length string. + new_mode (int): The state of the new mode. + unfilled_length (int): The state of the current unfilled_length. + + Returns: + tuple: (cost, new_length). + + """ + encoder_class = encoders[new_mode] + character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class] + if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]: + new_length = 1 + elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]: + new_length = 0 + cost = encoder_class.length(character, character_count_indicator_length) + return (cost, new_length) + + def _find_best(self, data): + """Find the index which has the minimum costs. + + Args: + data (str): The data to encode. + + Returns: + dict: The dict object includes "cost" and "index". The "cost" is the value of minimum cost. + The "index" is the index of the dp table as a tuple (n, mode, unfilled_length). + + """ + best = self.INF + best_index = (-1, -1) + for mode in range(4): + 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 {"cost": best, "index": best_index} + + def _reconstruct_path(self, best_index): + """Reconstructs the path. + + Args: + best_index: The best index computed by self._find_best(). + + Returns: + list: The path of minimum cost in the dynamic programming table. + + """ + path = [] + index = best_index + while index[0] != 0: + path.append(index) + index = self.parents[index[0]][index[1]][index[2]] + path.reverse() + return path + + def _compute_segments(self, path, data): + """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 + 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 diff --git a/src/rmqrcode/util/error_correction.py b/src/rmqrcode/util/error_correction.py index a3b47de..14943a5 100644 --- a/src/rmqrcode/util/error_correction.py +++ b/src/rmqrcode/util/error_correction.py @@ -1,12 +1,13 @@ -from .utilities import msb, to_binary from .galois_fields import GaloisFields +from .utilities import msb, to_binary + def compute_bch(data): data <<= 12 - g = 1<<12 | 1<<11 | 1<<10 | 1<<9 | 1<<8 | 1<<5 | 1<<2 | 1<<0 + g = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0 tmp_data = data - while (msb(tmp_data) >= 13): + while msb(tmp_data) >= 13: multiple = msb(tmp_data) - 13 tmp_g = g << multiple tmp_data ^= tmp_g @@ -14,6 +15,8 @@ def compute_bch(data): gf = GaloisFields() + + def compute_reed_solomon(data, g, num_error_codewords): f = list(map(lambda x: int(x, 2), data)) @@ -21,13 +24,14 @@ def compute_reed_solomon(data, g, num_error_codewords): f.append(0) for i in range(len(data)): - if f[i] == 0: continue + if f[i] == 0: + continue mult = gf.i2e[f[i]] for j in range(len(g)): - f[i+j] ^= gf.e2i[(g[j]+mult)%255] + f[i + j] ^= gf.e2i[(g[j] + mult) % 255] rs_codewords = [] for i in range(num_error_codewords): rs_codewords.append(to_binary(f[-num_error_codewords + i], 8)) - return rs_codewords \ No newline at end of file + return rs_codewords diff --git a/src/rmqrcode/util/galois_fields.py b/src/rmqrcode/util/galois_fields.py index c23e210..4a0cdf7 100644 --- a/src/rmqrcode/util/galois_fields.py +++ b/src/rmqrcode/util/galois_fields.py @@ -4,8 +4,8 @@ class GaloisFields: i2e = {} def __init__(self): - # GF(2^8)の既約多項式 - p = (1<<8)|(1<<4)|(1<<3)|(1<<2)|1 + # Irreducible polynomial in GF(2^8) + p = (1 << 8) | (1 << 4) | (1 << 3) | (1 << 2) | 1 self.e2i[0] = 1 self.e2i[255] = 1 @@ -15,7 +15,7 @@ def __init__(self): tmp = 1 for e in range(1, 255): tmp <<= 1 - if tmp & (1<<8): + if tmp & (1 << 8): tmp ^= p self.e2i[e] = tmp self.i2e[tmp] = e diff --git a/src/rmqrcode/util/utilities.py b/src/rmqrcode/util/utilities.py index 8de88ac..1f4f4c4 100644 --- a/src/rmqrcode/util/utilities.py +++ b/src/rmqrcode/util/utilities.py @@ -8,9 +8,9 @@ def to_binary(data, len): def split_into_8bits(data): codewords = [] - while (len(data) >= 8): + while len(data) >= 8: codewords.append(data[:8]) data = data[8:] if data != "": - codewords.append(data.ljust(8, '0')) - return codewords \ No newline at end of file + codewords.append(data.ljust(8, "0")) + return codewords diff --git a/tests/encoder/alphanumeric_encoder_test.py b/tests/encoder/alphanumeric_encoder_test.py new file mode 100644 index 0000000..1f621a2 --- /dev/null +++ b/tests/encoder/alphanumeric_encoder_test.py @@ -0,0 +1,22 @@ +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 + assert AlphanumericEncoder.is_valid_characters("📌") is False 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 new file mode 100644 index 0000000..debb7fd --- /dev/null +++ b/tests/encoder/kanji_encoder_test.py @@ -0,0 +1,22 @@ +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 + assert KanjiEncoder.is_valid_characters("📌") is False diff --git a/tests/encoder/numeric_encoder_test.py b/tests/encoder/numeric_encoder_test.py new file mode 100644 index 0000000..4870e19 --- /dev/null +++ b/tests/encoder/numeric_encoder_test.py @@ -0,0 +1,22 @@ +from rmqrcode.encoder import NumericEncoder, IllegalCharacterError + +import pytest + + +class TestNumericEncoder: + 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 + + 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 diff --git a/tests/rmqrcode_test.py b/tests/rmqrcode_test.py index b857821..58d4361 100644 --- a/tests/rmqrcode_test.py +++ b/tests/rmqrcode_test.py @@ -1,7 +1,11 @@ -from rmqrcode import rMQR -from rmqrcode import ErrorCorrectionLevel -from rmqrcode import DataTooLongError -from rmqrcode import IllegalVersionError +from rmqrcode import ( + rMQR, + encoder, + ErrorCorrectionLevel, + DataTooLongError, + IllegalVersionError, + NoSegmentError, +) import pytest @@ -11,13 +15,76 @@ 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() - def test_raise_too_long_error(self): + assert len(qr.to_list(with_quiet_zone=True)) is 17 + assert len(qr.to_list(with_quiet_zone=True)[0]) is 103 + + assert len(qr.to_list(with_quiet_zone=False)) is 13 + assert len(qr.to_list(with_quiet_zone=False)[0]) is 99 + + def test_raise_no_segment_error(self): + with pytest.raises(NoSegmentError) as e: + qr = rMQR("R13x99", ErrorCorrectionLevel.M) + qr.make() + + 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) + 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..552dd27 --- /dev/null +++ b/tests/segments_test.py @@ -0,0 +1,59 @@ +from rmqrcode.segments import SegmentOptimizer, compute_length +from rmqrcode import encoder, ErrorCorrectionLevel, DataTooLongError +import pytest + + +class TestSegments: + def test_can_optimize_segments_numeric_and_byte(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "123", "encoder_class": encoder.NumericEncoder}, + {"data": "Abc", "encoder_class": encoder.ByteEncoder}, + ] + + def test_can_optimize_segments_alphanumeric_and_kanji(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("17:30集合", "R7x59", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "17:30", "encoder_class": encoder.AlphanumericEncoder}, + {"data": "集合", "encoder_class": encoder.KanjiEncoder}, + ] + + def test_can_optimize_segments_numeric_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123456", "R7x59", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "123456", "encoder_class": encoder.NumericEncoder}, + ] + + def test_can_optimize_segments_alphanumeric_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("HTTPS://", "R7x59", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "HTTPS://", "encoder_class": encoder.AlphanumericEncoder}, + ] + + def test_can_optimize_segments_byte_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("1+zY!a:K", "R7x59", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "1+zY!a:K", "encoder_class": encoder.ByteEncoder}, + ] + + def test_can_optimize_segments_kanji_only(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("漢字", "R7x59", ErrorCorrectionLevel.M) + assert segments == [ + {"data": "漢字", "encoder_class": encoder.KanjiEncoder}, + ] + + def test_optimize_segments_raises_data_too_long_error(self): + optimizer = SegmentOptimizer() + with pytest.raises(DataTooLongError) as e: + segments = optimizer.compute("a" * 12, "R7x59", ErrorCorrectionLevel.M) + + def test_compute_length(self): + optimizer = SegmentOptimizer() + segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M) + assert compute_length(segments, "R7x43") is 47