├── requirements.txt ├── MANIFEST.in ├── requirements-test.txt ├── LICENSE ├── setup.py ├── .gitignore ├── .github └── workflows │ └── test_and_release.yml ├── README.md ├── test_aztec_code_generator.py └── aztec_code_generator.py /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include description.rst 2 | include requirements.txt 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pillow>=3.0,<6.0; python_version < '3.5' 2 | pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6' 3 | pillow>=8.0; python_version >= '3.6' 4 | zxing>=0.13 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dmitry Alimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | if sys.version_info < (3,4): 9 | sys.exit("Python 3.4+ is required; you are using %s" % sys.version) 10 | 11 | setup(name="aztec_code_generator", 12 | version="0.11", 13 | description='Aztec Code generator in Python', 14 | long_description=open('README.md').read(), 15 | long_description_content_type='text/markdown', 16 | author='Dmitry Alimov', 17 | author_email="dvalimov@gmail.com", 18 | maintainer='Daniel Lenski', 19 | maintainer_email='dlenski@gmail.com', 20 | install_requires=open('requirements.txt').readlines(), 21 | extras_require={ 22 | "Image": [ 23 | "pillow>=3.0,<6.0; python_version < '3.5'", 24 | "pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6'", 25 | "pillow>=8.0; python_version >= '3.6'", 26 | ] 27 | }, 28 | tests_require=open('requirements-test.txt').readlines(), 29 | license='MIT', 30 | url="https://github.com/dlenski/aztec_code_generator", 31 | py_modules=["aztec_code_generator"], 32 | ) 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.github/workflows/test_and_release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: test_and_release 5 | 6 | on: [ push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.7', '3.8', '3.9', '3.10'] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | python -m pip install setuptools flake8 pytest 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Lint with flake8 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Build 34 | run: | 35 | python setup.py bdist 36 | 37 | test: 38 | 39 | runs-on: ubuntu-latest 40 | needs: build 41 | strategy: 42 | matrix: 43 | python-version: ['3.7', '3.8', '3.9', '3.10'] 44 | 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Set up Python 48 | uses: actions/setup-python@v2 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi 55 | - name: Test 56 | run: | 57 | python setup.py test 58 | 59 | # https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml 60 | release: 61 | 62 | runs-on: ubuntu-latest 63 | needs: test 64 | if: startsWith(github.ref, 'refs/tags/v') 65 | 66 | steps: 67 | - uses: actions/checkout@v2 68 | - name: Set up Python 69 | uses: actions/setup-python@v2 70 | with: 71 | python-version: '3.x' 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install --upgrade pip 75 | pip install setuptools wheel twine 76 | - name: Deploy to PyPI 77 | env: 78 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 79 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 80 | run: | 81 | python setup.py sdist bdist_wheel 82 | twine upload dist/* 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aztec Code generator 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/aztec_code_generator.svg)](https://pypi.python.org/pypi/aztec_code_generator) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | [![Build Status](https://github.com/dlenski/aztec_code_generator/workflows/test_and_release/badge.svg)](https://github.com/dlenski/aztec_code_generator/actions?query=workflow%3Atest_and_release) 6 | 7 | This is a pure-Python library to generate [Aztec Code](https://en.wikipedia.org/wiki/Aztec_code) 2D barcodes. 8 | 9 | ## Changelog 10 | 11 | - `v0.1`-`v0.2`: initial Python packaging 12 | - `v0.3`: allow optional border, more efficient matrix representation 13 | - `v0.4`: merge https://github.com/delimitry/aztec_code_generator/pull/5 and fix tests 14 | - `v0.5`: 15 | - code simplification 16 | - more efficient internal data structures (`Enum`) 17 | - encoding of `FLG(n)` 18 | - correct handling of Python 3 `str` vs. `bytes` (Aztec Code natively encodes _bytes_, not characters, and a reader's default interpretation of those bytes should be [ISO-8859-1 aka Latin-1](https://en.wikipedia.org/wiki/Iso-8859-1)) 19 | - `v0.6`: 20 | - more code simplification 21 | - make Pillow dependency optional 22 | - add `print_fancy` for UTF-8 output (inspired by `qrencode -t ansiutf8`) 23 | - bugfix for `DIGIT`→`PUNCT` transition (and add missed test case) 24 | - allow customization of error correction percentage level 25 | - `v0.7`: 26 | - support standard-compliant encoding of strings in character sets other than [ISO-8859-1](https://en.wikipedia.org/wiki/ISO-8859-1) 27 | via [ECI indications](https://en.wikipedia.org/wiki/Extended_Channel_Interpretation) 28 | - `v0.8`-`v0.9`: 29 | - replace Travis-CI with Github Actions for CI 30 | - `v0.10` 31 | - bugfix for lowercase → uppercase transition (fixes encoding of strings like `abcABC`) 32 | - `v0.11` 33 | - fix docstrings 34 | - change default `module_size` in image output to 2 pixels; ZXing can't read with `module_size=1` 35 | 36 | 37 | ## Installation 38 | 39 | Releases [from PyPi](https://pypi.org/project/aztec-code-generator/) may be installed with `pip3 install aztec_code_generator`. 40 | 41 | Bleeding-edge version from `master` branch of this repository can be installed with 42 | `pip3 install https://github.com/dlenski/aztec_code_generator/archive/master.zip`. 43 | 44 | ### Dependencies 45 | 46 | [Pillow](https://pillow.readthedocs.io) (Python image generation library) is required if you want to generate image objects and files. 47 | 48 | ## Usage 49 | 50 | ### Creating and encoding 51 | 52 | ```python 53 | from aztec_code_generator import AztecCode 54 | data = 'Aztec Code 2D :)' 55 | aztec_code = AztecCode(data) 56 | ``` 57 | 58 | The `AztecCode()` constructor takes additional, optional arguments: 59 | 60 | - `size` and `compact`: to set a specific symbol size (e.g. `19, True` for a compact 19×19 symbol); see `keys(aztec_code_generator.configs)` for possible values 61 | - `ec_percent` for error correction percentage (default is the recommended 23), plus `size` a 62 | 63 | ### Saving an image file 64 | 65 | `aztec_code.save('aztec_code.png', module_size=4, border=1)` will save an image file `aztec_code.png` of the symbol, with 4×4 blocks of white/black pixels in 66 | the output, and with a 1-block border. 67 | 68 | ![Aztec Code](https://1.bp.blogspot.com/-OZIo4dGwAM4/V7BaYoBaH2I/AAAAAAAAAwc/WBdTV6osTb4TxNf2f6v7bCfXM4EuO4OdwCLcB/s1600/aztec_code.png "Aztec Code with data") 69 | 70 | ### Creating an image object 71 | 72 | `aztec_code.image()` will yield a monochrome-mode [PIL `Image` object](https://pillow.readthedocs.io/en/stable/reference/Image.html) representing the image 73 | in-memory. It also accepts optional `module_size` and `border`. 74 | 75 | ### Text-based output 76 | 77 | `aztec_code.print_fancy()` will print the resulting Aztec Code to standard output using 78 | [Unicode half-height block elements](https://en.wikipedia.org/wiki/Block_Elements) encoded 79 | with UTF-8 and ANSI color escapes. It accepts optional `border`. 80 | 81 | `aztec_code.print_out()` will print out the resulting Aztec Code to standard 82 | output as plain ASCII text, using `#` and ` ` characters: 83 | 84 | ``` 85 | ## # ## #### 86 | # ## ##### ### 87 | # ## # # # ### 88 | ## # # ## ## 89 | ## # # # # 90 | ## ############ # # 91 | ### # ### # 92 | ## # ##### # ## # 93 | # # # # ## 94 | # # # # # # ### 95 | ## # # ## ## 96 | #### # ##### ## # 97 | # ## ## ## 98 | ## ########### # 99 | ## # ## ## # 100 | ## # ### # ## 101 | ############ 102 | ## # # ## # 103 | ## # ## ### # 104 | ``` 105 | 106 | ## Authors: 107 | 108 | Originally written by [Dmitry Alimov (delimtry)](https://github.com/delimitry). 109 | 110 | Updates, bug fixes, Python 3-ification, and careful `bytes`-vs.-`str` handling 111 | by [Daniel Lenski (dlenski)](https://github.com/dlenski). 112 | 113 | ## License: 114 | 115 | Released under [The MIT License](https://github.com/delimitry/aztec_code_generator/blob/master/LICENSE). 116 | -------------------------------------------------------------------------------- /test_aztec_code_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #-*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from aztec_code_generator import ( 6 | reed_solomon, find_optimal_sequence, optimal_sequence_to_bits, get_data_codewords, encoding_to_eci, 7 | Mode, Latch, Shift, Misc, 8 | AztecCode, 9 | ) 10 | 11 | import codecs 12 | from tempfile import NamedTemporaryFile 13 | 14 | try: 15 | import zxing 16 | except ImportError: 17 | zxing = None 18 | 19 | def b(*l): 20 | return [(ord(c) if len(c)==1 else c.encode()) if isinstance(c, str) else c for c in l] 21 | 22 | class Test(unittest.TestCase): 23 | """ 24 | Test aztec_code_generator module 25 | """ 26 | 27 | def test_reed_solomon(self): 28 | """ Test reed_solomon function """ 29 | cw = [] 30 | reed_solomon(cw, 0, 0, 0, 0) 31 | self.assertEqual(cw, []) 32 | cw = [0, 0] + [0, 0] 33 | reed_solomon(cw, 2, 2, 16, 19) 34 | self.assertEqual(cw, [0, 0, 0, 0]) 35 | cw = [9, 50, 1, 41, 47, 2, 39, 37, 1, 27] + [0, 0, 0, 0, 0, 0, 0] 36 | reed_solomon(cw, 10, 7, 64, 67) 37 | self.assertEqual(cw, [9, 50, 1, 41, 47, 2, 39, 37, 1, 27, 38, 50, 8, 16, 10, 20, 40]) 38 | cw = [0, 9] + [0, 0, 0, 0, 0] 39 | reed_solomon(cw, 2, 5, 16, 19) 40 | self.assertEqual(cw, [0, 9, 12, 2, 3, 1, 9]) 41 | 42 | def test_find_optimal_sequence_ascii_strings(self): 43 | """ Test find_optimal_sequence function for ASCII strings """ 44 | self.assertEqual(find_optimal_sequence(''), b()) 45 | self.assertEqual(find_optimal_sequence('ABC'), b('A', 'B', 'C')) 46 | self.assertEqual(find_optimal_sequence('abc'), b(Latch.LOWER, 'a', 'b', 'c')) 47 | self.assertEqual(find_optimal_sequence('Wikipedia, the free encyclopedia'), b( 48 | 'W', Latch.LOWER, 'i', 'k', 'i', 'p', 'e', 'd', 'i', 'a', Shift.PUNCT, ', ', 't', 'h', 'e', 49 | ' ', 'f', 'r', 'e', 'e', ' ', 'e', 'n', 'c', 'y', 'c', 'l', 'o', 'p', 'e', 'd', 'i', 'a')) 50 | self.assertEqual(find_optimal_sequence('Code 2D!'), b( 51 | 'C', Latch.LOWER, 'o', 'd', 'e', Latch.DIGIT, ' ', '2', Shift.UPPER, 'D', Shift.PUNCT, '!')) 52 | self.assertEqual(find_optimal_sequence('!#$%&?'), b(Latch.MIXED, Latch.PUNCT, '!', '#', '$', '%', '&', '?')) 53 | 54 | self.assertIn(find_optimal_sequence('. : '), ( 55 | b(Shift.PUNCT, '. ', Shift.PUNCT, ': '), 56 | b(Latch.MIXED, Latch.PUNCT, '. ', ': ') )) 57 | self.assertEqual(find_optimal_sequence('\r\n\r\n\r\n'), b(Latch.MIXED, Latch.PUNCT, '\r\n', '\r\n', '\r\n')) 58 | self.assertEqual(find_optimal_sequence('Code 2D!'), b( 59 | 'C', Latch.LOWER, 'o', 'd', 'e', Latch.DIGIT, ' ', '2', Shift.UPPER, 'D', Shift.PUNCT, '!')) 60 | self.assertEqual(find_optimal_sequence('test 1!test 2!'), b( 61 | Latch.LOWER, 't', 'e', 's', 't', Latch.DIGIT, ' ', '1', Shift.PUNCT, '!', Latch.UPPER, 62 | Latch.LOWER, 't', 'e', 's', 't', Latch.DIGIT, ' ', '2', Shift.PUNCT, '!')) 63 | self.assertEqual(find_optimal_sequence('Abc-123X!Abc-123X!'), b( 64 | 'A', Latch.LOWER, 'b', 'c', Latch.DIGIT, Shift.PUNCT, '-', '1', '2', '3', Latch.UPPER, 'X', Shift.PUNCT, '!', 65 | 'A', Latch.LOWER, 'b', 'c', Latch.DIGIT, Shift.PUNCT, '-', '1', '2', '3', Shift.UPPER, 'X', Shift.PUNCT, '!')) 66 | self.assertEqual(find_optimal_sequence('ABCabc1a2b3e'), b( 67 | 'A', 'B', 'C', Latch.LOWER, 'a', 'b', 'c', Shift.BINARY, 5, '1', 'a', '2', 'b', '3', 'e')) 68 | self.assertEqual(find_optimal_sequence('ABCabc1a2b3eBC'), b( 69 | 'A', 'B', 'C', Latch.LOWER, 'a', 'b', 'c', Shift.BINARY, 6, '1', 'a', '2', 'b', '3', 'e', Latch.DIGIT, Latch.UPPER, 'B', 'C')) 70 | self.assertEqual(find_optimal_sequence('abcABC'), b( 71 | Latch.LOWER, 'a', 'b', 'c', Latch.DIGIT, Latch.UPPER, 'A', 'B', 'C')) 72 | self.assertEqual(find_optimal_sequence('0a|5Tf.l'), b( 73 | Shift.BINARY, 5, '0', 'a', '|', '5', 'T', Latch.LOWER, 'f', Shift.PUNCT, '.', 'l')) 74 | self.assertEqual(find_optimal_sequence('*V1\x0c {Pa'), b( 75 | Shift.PUNCT, '*', 'V', Shift.BINARY, 5, '1', '\x0c', ' ', '{', 'P', Latch.LOWER, 'a')) 76 | self.assertEqual(find_optimal_sequence('~Fxlb"I4'), b( 77 | Shift.BINARY, 7, '~', 'F', 'x', 'l', 'b', '"', 'I', Latch.DIGIT, '4')) 78 | self.assertEqual(find_optimal_sequence('\\+=R?1'), b( 79 | Latch.MIXED, '\\', Latch.PUNCT, '+', '=', Latch.UPPER, 'R', Latch.DIGIT, Shift.PUNCT, '?', '1')) 80 | self.assertEqual(find_optimal_sequence('0123456789:;<=>'), b( 81 | Latch.DIGIT, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', Latch.UPPER, Latch.MIXED, Latch.PUNCT, ':', ';', '<', '=', '>')) 82 | 83 | def test_encodings_canonical(self): 84 | for encoding in encoding_to_eci: 85 | self.assertEqual(encoding, codecs.lookup(encoding).name) 86 | 87 | def _optimal_eci_sequence(self, charset): 88 | eci = encoding_to_eci[charset] 89 | ecis = str(eci) 90 | return [ Shift.PUNCT, Misc.FLG, len(ecis), eci ] 91 | 92 | def test_find_optimal_sequence_non_ASCII_strings(self): 93 | """ Test find_optimal_sequence function for non-ASCII strings""" 94 | 95 | # Implicit iso8559-1 without ECI: 96 | self.assertEqual(find_optimal_sequence('Français'), b( 97 | 'F', Latch.LOWER, 'r', 'a', 'n', Shift.BINARY, 1, 0xe7, 'a', 'i', 's')) 98 | 99 | # ECI: explicit iso8859-1, cp1252 (Windows-1252), and utf-8 100 | self.assertEqual(find_optimal_sequence('Français', 'iso8859-1'), self._optimal_eci_sequence('iso8859-1') + b( 101 | 'F', Latch.LOWER, 'r', 'a', 'n', Shift.BINARY, 1, 0xe7, 'a', 'i', 's')) 102 | self.assertEqual(find_optimal_sequence('€800', 'cp1252'), self._optimal_eci_sequence('cp1252') + b( 103 | Shift.BINARY, 1, 0x80, Latch.DIGIT, '8', '0', '0')) 104 | self.assertEqual(find_optimal_sequence('Français', 'utf-8'), self._optimal_eci_sequence('utf-8') + b( 105 | 'F', Latch.LOWER, 'r', 'a', 'n', Shift.BINARY, 2, 0xc3, 0xa7, 'a', 'i', 's')) 106 | 107 | def test_find_optimal_sequence_bytes(self): 108 | """ Test find_optimal_sequence function for byte strings """ 109 | 110 | self.assertEqual(find_optimal_sequence(b'a' + b'\xff' * 31 + b'A'), b( 111 | Shift.BINARY, 0, 1, 'a') + [0xff] * 31 + b('A')) 112 | self.assertEqual(find_optimal_sequence(b'abc' + b'\xff' * 32 + b'A'), b( 113 | Latch.LOWER, 'a', 'b', 'c', Shift.BINARY, 0, 1) + [0xff] * 32 + b(Latch.DIGIT, Latch.UPPER, 'A')) 114 | self.assertEqual(find_optimal_sequence(b'abc' + b'\xff' * 31 + b'@\\\\'), b( 115 | Latch.LOWER, 'a', 'b', 'c', Shift.BINARY, 31) + [0xff] * 31 + b(Latch.MIXED, '@', '\\', '\\')) 116 | self.assertEqual(find_optimal_sequence(b'!#$%&?\xff'), b( 117 | Latch.MIXED, Latch.PUNCT, '!', '#', '$', '%', '&', '?', Latch.UPPER, Shift.BINARY, 1, '\xff')) 118 | self.assertEqual(find_optimal_sequence(b'!#$%&\xff'), b(Shift.BINARY, 6, '!', '#', '$', '%', '&', '\xff')) 119 | self.assertEqual(find_optimal_sequence(b'@\xff'), b(Shift.BINARY, 2, '@', '\xff')) 120 | self.assertEqual(find_optimal_sequence(b'. @\xff'), b(Shift.PUNCT, '. ', Shift.BINARY, 2, '@', '\xff')) 121 | 122 | def test_find_optimal_sequence_CRLF_bug(self): 123 | """ Demonstrate a known bug in find_optimal_sequence (https://github.com/dlenski/aztec_code_generator/pull/4) 124 | 125 | This is a much more minimal example of https://github.com/delimitry/aztec_code_generator/issues/7 126 | 127 | The string '\t<\r\n': 128 | SHOULD be sequenced as: Latch.MIXED '\t' Latch.PUNCT < '\r' '\n' 129 | but is incorrectly sequenced as: Latch.MIXED '\t' Shift.PUNCT < '\r\n' 130 | 131 | ... which is impossible since no encoding of the 2 byte sequence b'\r\n' exists in MIXED mode. """ 132 | 133 | self.assertEqual(find_optimal_sequence(b'\t<\r\n'), b( 134 | Latch.MIXED, '\t', Latch.PUNCT, '<', '\r\n' 135 | )) 136 | 137 | def test_optimal_sequence_to_bits(self): 138 | """ Test optimal_sequence_to_bits function """ 139 | self.assertEqual(optimal_sequence_to_bits(b()), '') 140 | self.assertEqual(optimal_sequence_to_bits(b(Shift.PUNCT)), '00000') 141 | self.assertEqual(optimal_sequence_to_bits(b('A')), '00010') 142 | self.assertEqual(optimal_sequence_to_bits(b(Shift.BINARY, 1, '\xff')), '111110000111111111') 143 | self.assertEqual(optimal_sequence_to_bits(b(Shift.BINARY, 0, 1) + [0xff] * 32), '111110000000000000001' + '11111111'*32) 144 | self.assertEqual(optimal_sequence_to_bits(b(Shift.PUNCT, Misc.FLG, 0, 'A')), '000000000000000010') 145 | self.assertEqual(optimal_sequence_to_bits(b(Shift.PUNCT, Misc.FLG, 1, 3, 'A')), '0000000000001' + '0101' + '00010') # FLG(1) '3' 146 | self.assertEqual(optimal_sequence_to_bits(b(Shift.PUNCT, Misc.FLG, 6, 3, 'A')), '0000000000110' + '0010'*5 + '0101' + '00010') # FLG(6) '000003' 147 | 148 | def test_get_data_codewords(self): 149 | """ Test get_data_codewords function """ 150 | self.assertEqual(get_data_codewords('000010', 6), [0b000010]) 151 | self.assertEqual(get_data_codewords('111100', 6), [0b111100]) 152 | self.assertEqual(get_data_codewords('111110', 6), [0b111110, 0b011111]) 153 | self.assertEqual(get_data_codewords('000000', 6), [0b000001, 0b011111]) 154 | self.assertEqual(get_data_codewords('111111', 6), [0b111110, 0b111110]) 155 | self.assertEqual(get_data_codewords('111101111101', 6), [0b111101, 0b111101]) 156 | 157 | def _encode_and_decode(self, reader, data, *args, **kwargs): 158 | with NamedTemporaryFile(suffix='.png') as f: 159 | code = AztecCode(data, *args, **kwargs) 160 | code.save(f, module_size=5) 161 | result = reader.decode(f.name, **(dict(encoding=None) if isinstance(data, bytes) else {})) 162 | assert result is not None 163 | self.assertEqual(data, result.raw) 164 | 165 | @unittest.skipUnless(zxing, reason='Python module zxing cannot be imported; cannot test decoding.') 166 | def test_barcode_readability(self): 167 | r = zxing.BarCodeReader() 168 | 169 | # FIXME: ZXing command-line runner tries to coerce everything to UTF-8, at least on Linux, 170 | # so we can only reliably encode and decode characters that are in the intersection of utf-8 171 | # and iso8559-1 (though with ZXing >=3.5, the iso8559-1 requirement is relaxed; see below). 172 | # 173 | # More discussion at: https://github.com/dlenski/python-zxing/issues/17#issuecomment-905728212 174 | # Proposed solution: https://github.com/dlenski/python-zxing/issues/19 175 | self._encode_and_decode(r, 'Wikipedia, the free encyclopedia', ec_percent=0) 176 | self._encode_and_decode(r, 'Wow. Much error. Very correction. Amaze', ec_percent=95) 177 | self._encode_and_decode(r, '¿Cuánto cuesta?') 178 | 179 | @unittest.skipUnless(zxing, reason='Python module zxing cannot be imported; cannot test decoding.') 180 | def test_barcode_readability_eci(self): 181 | r = zxing.BarCodeReader() 182 | 183 | # ZXing <=3.4.1 doesn't correctly decode ECI or FNC1 in Aztec (https://github.com/zxing/zxing/issues/1327), 184 | # so we don't have a way to test readability of barcodes containing characters not in iso8559-1. 185 | # ZXing 3.5.0 includes my contribution to decode Aztec codes with non-default charsets (https://github.com/zxing/zxing/pull/1328) 186 | if r.zxing_version_info < (3, 5): 187 | raise unittest.SkipTest("Running with ZXing v{}. In order to decode non-iso8859-1 charsets in Aztec Code, we need v3.5+".format(r.zxing_version)) 188 | 189 | self._encode_and_decode(r, 'The price is €4', encoding='utf-8') 190 | self._encode_and_decode(r, 'אין לי מושג', encoding='iso8859-8') 191 | 192 | 193 | if __name__ == '__main__': 194 | unittest.main(verbosity=2) 195 | -------------------------------------------------------------------------------- /aztec_code_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #-*- coding: utf-8 -*- 3 | """ 4 | aztec_code_generator 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Aztec code generator. 8 | 9 | :copyright: (c) 2016-2018 by Dmitry Alimov. 10 | :license: The MIT License (MIT), see LICENSE for more details. 11 | """ 12 | 13 | import math 14 | import numbers 15 | import sys 16 | import array 17 | import codecs 18 | from collections import namedtuple 19 | from enum import Enum 20 | 21 | try: 22 | from PIL import Image, ImageDraw 23 | except ImportError: 24 | Image = ImageDraw = None 25 | missing_pil = sys.exc_info() 26 | 27 | try: 28 | from StringIO import StringIO 29 | except ImportError: 30 | from io import StringIO 31 | 32 | Config = namedtuple('Config', ('layers', 'codewords', 'cw_bits', 'bits')) 33 | 34 | configs = { 35 | (15, True): Config(layers=1, codewords=17, cw_bits=6, bits=102), 36 | (19, False): Config(layers=1, codewords=21, cw_bits=6, bits=126), 37 | (19, True): Config(layers=2, codewords=40, cw_bits=6, bits=240), 38 | (23, False): Config(layers=2, codewords=48, cw_bits=6, bits=288), 39 | (23, True): Config(layers=3, codewords=51, cw_bits=8, bits=408), 40 | (27, False): Config(layers=3, codewords=60, cw_bits=8, bits=480), 41 | (27, True): Config(layers=4, codewords=76, cw_bits=8, bits=608), 42 | (31, False): Config(layers=4, codewords=88, cw_bits=8, bits=704), 43 | (37, False): Config(layers=5, codewords=120, cw_bits=8, bits=960), 44 | (41, False): Config(layers=6, codewords=156, cw_bits=8, bits=1248), 45 | (45, False): Config(layers=7, codewords=196, cw_bits=8, bits=1568), 46 | (49, False): Config(layers=8, codewords=240, cw_bits=8, bits=1920), 47 | (53, False): Config(layers=9, codewords=230, cw_bits=10, bits=2300), 48 | (57, False): Config(layers=10, codewords=272, cw_bits=10, bits=2720), 49 | (61, False): Config(layers=11, codewords=316, cw_bits=10, bits=3160), 50 | (67, False): Config(layers=12, codewords=364, cw_bits=10, bits=3640), 51 | (71, False): Config(layers=13, codewords=416, cw_bits=10, bits=4160), 52 | (75, False): Config(layers=14, codewords=470, cw_bits=10, bits=4700), 53 | (79, False): Config(layers=15, codewords=528, cw_bits=10, bits=5280), 54 | (83, False): Config(layers=16, codewords=588, cw_bits=10, bits=5880), 55 | (87, False): Config(layers=17, codewords=652, cw_bits=10, bits=6520), 56 | (91, False): Config(layers=18, codewords=720, cw_bits=10, bits=7200), 57 | (95, False): Config(layers=19, codewords=790, cw_bits=10, bits=7900), 58 | (101, False): Config(layers=20, codewords=864, cw_bits=10, bits=8640), 59 | (105, False): Config(layers=21, codewords=940, cw_bits=10, bits=9400), 60 | (109, False): Config(layers=22, codewords=1020, cw_bits=10, bits=10200), 61 | (113, False): Config(layers=23, codewords=920, cw_bits=12, bits=11040), 62 | (117, False): Config(layers=24, codewords=992, cw_bits=12, bits=11904), 63 | (121, False): Config(layers=25, codewords=1066, cw_bits=12, bits=12792), 64 | (125, False): Config(layers=26, codewords=1144, cw_bits=12, bits=13728), 65 | (131, False): Config(layers=27, codewords=1224, cw_bits=12, bits=14688), 66 | (135, False): Config(layers=28, codewords=1306, cw_bits=12, bits=15672), 67 | (139, False): Config(layers=29, codewords=1392, cw_bits=12, bits=16704), 68 | (143, False): Config(layers=30, codewords=1480, cw_bits=12, bits=17760), 69 | (147, False): Config(layers=31, codewords=1570, cw_bits=12, bits=18840), 70 | (151, False): Config(layers=32, codewords=1664, cw_bits=12, bits=19968), 71 | } 72 | 73 | encoding_to_eci = { 74 | 'cp437': 0, # also 2 75 | 'iso8859-1': 1, # (also 3) default interpretation, readers should assume if no ECI mark 76 | 'iso8859-2': 4, 77 | 'iso8859-3': 5, 78 | 'iso8859-4': 6, 79 | 'iso8859-5': 7, 80 | 'iso8859-6': 8, 81 | 'iso8859-7': 9, 82 | 'iso8859-8': 10, 83 | 'iso8859-9': 11, 84 | 'iso8859-13': 15, 85 | 'iso8859-14': 16, 86 | 'iso8859-15': 17, 87 | 'iso8859-16': 18, 88 | 'shift_jis': 20, 89 | 'cp1250': 21, 90 | 'cp1251': 22, 91 | 'cp1252': 23, 92 | 'cp1256': 24, 93 | 'utf-16-be': 25, # no BOM 94 | 'utf-8': 26, 95 | 'ascii': 27, # also 170 96 | 'big5': 28, 97 | 'gb18030': 29, 98 | 'euc_kr': 30, 99 | } 100 | 101 | polynomials = { 102 | 4: 19, 103 | 6: 67, 104 | 8: 301, 105 | 10: 1033, 106 | 12: 4201, 107 | } 108 | 109 | Side = Enum('Side', ('left', 'right', 'bottom', 'top')) 110 | 111 | Mode = Enum('Mode', ('UPPER', 'LOWER', 'MIXED', 'PUNCT', 'DIGIT', 'BINARY')) 112 | Latch = Enum('Latch', Mode.__members__) 113 | Shift = Enum('Shift', Mode.__members__) 114 | Misc = Enum('Misc', ('FLG', 'SIZE', 'RESUME')) 115 | 116 | code_chars = { 117 | Mode.UPPER: [Shift.PUNCT] + list(b' ABCDEFGHIJKLMNOPQRSTUVWXYZ') + [Latch.LOWER, Latch.MIXED, Latch.DIGIT, Shift.BINARY], 118 | Mode.LOWER: [Shift.PUNCT] + list(b' abcdefghijklmnopqrstuvwxyz') + [Shift.UPPER, Latch.MIXED, Latch.DIGIT, Shift.BINARY], 119 | Mode.MIXED: [Shift.PUNCT] + list(b' \x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x1b\x1c\x1d\x1e\x1f@\\^_`|~\x7f') + [Latch.LOWER, Latch.UPPER, Latch.PUNCT, Shift.BINARY], 120 | Mode.PUNCT: [Misc.FLG] + list(b'\r') + [b'\r\n', b'. ', b', ', b': '] + list(b'!"#$%&\'()*+,-./:;<=>?[]{}') + [Latch.UPPER], 121 | Mode.DIGIT: [Shift.PUNCT] + list(b' 0123456789,.') + [Latch.UPPER, Shift.UPPER], 122 | } 123 | 124 | punct_2_chars = [pc for pc in code_chars[Mode.PUNCT] if isinstance(pc, bytes)] 125 | 126 | E = 99999 # some big number 127 | 128 | latch_len = { 129 | Mode.UPPER: { 130 | Mode.UPPER: 0, Mode.LOWER: 5, Mode.MIXED: 5, Mode.PUNCT: 10, Mode.DIGIT: 5, Mode.BINARY: 10 131 | }, 132 | Mode.LOWER: { 133 | Mode.UPPER: 9, Mode.LOWER: 0, Mode.MIXED: 5, Mode.PUNCT: 10, Mode.DIGIT: 5, Mode.BINARY: 10 134 | }, 135 | Mode.MIXED: { 136 | Mode.UPPER: 5, Mode.LOWER: 5, Mode.MIXED: 0, Mode.PUNCT: 5, Mode.DIGIT: 10, Mode.BINARY: 10 137 | }, 138 | Mode.PUNCT: { 139 | Mode.UPPER: 5, Mode.LOWER: 10, Mode.MIXED: 10, Mode.PUNCT: 0, Mode.DIGIT: 10, Mode.BINARY: 15 140 | }, 141 | Mode.DIGIT: { 142 | Mode.UPPER: 4, Mode.LOWER: 9, Mode.MIXED: 9, Mode.PUNCT: 14, Mode.DIGIT: 0, Mode.BINARY: 14 143 | }, 144 | Mode.BINARY: { 145 | Mode.UPPER: 0, Mode.LOWER: 0, Mode.MIXED: 0, Mode.PUNCT: 0, Mode.DIGIT: 0, Mode.BINARY: 0 146 | }, 147 | } 148 | 149 | shift_len = { 150 | (Mode.UPPER, Mode.PUNCT): 5, 151 | (Mode.LOWER, Mode.UPPER): 5, 152 | (Mode.LOWER, Mode.PUNCT): 5, 153 | (Mode.MIXED, Mode.PUNCT): 5, 154 | (Mode.DIGIT, Mode.UPPER): 4, 155 | (Mode.DIGIT, Mode.PUNCT): 4, 156 | } 157 | 158 | char_size = { 159 | Mode.UPPER: 5, Mode.LOWER: 5, Mode.MIXED: 5, Mode.PUNCT: 5, Mode.DIGIT: 4, Mode.BINARY: 8, 160 | } 161 | 162 | abbr_modes = {m.name[0]:m for m in Mode} 163 | 164 | 165 | def prod(x, y, log, alog, gf): 166 | """ Product x times y """ 167 | if not x or not y: 168 | return 0 169 | return alog[(log[x] + log[y]) % (gf - 1)] 170 | 171 | 172 | def reed_solomon(wd, nd, nc, gf, pp): 173 | """ Calculate error correction codewords 174 | 175 | Algorithm is based on Aztec Code bar code symbology specification from 176 | GOST-R-ISO-MEK-24778-2010 (Russian) 177 | Takes ``nd`` data codeword values in ``wd`` and adds on ``nc`` check 178 | codewords, all within GF(gf) where ``gf`` is a power of 2 and ``pp`` 179 | is the value of its prime modulus polynomial. 180 | 181 | :param wd: data codewords (in/out param) 182 | :param nd: number of data codewords 183 | :param nc: number of error correction codewords 184 | :param gf: Galois Field order 185 | :param pp: prime modulus polynomial value 186 | """ 187 | # generate log and anti log tables 188 | log = {0: 1 - gf} 189 | alog = {0: 1} 190 | for i in range(1, gf): 191 | alog[i] = alog[i - 1] * 2 192 | if alog[i] >= gf: 193 | alog[i] ^= pp 194 | log[alog[i]] = i 195 | # generate polynomial coeffs 196 | c = {0: 1} 197 | for i in range(1, nc + 1): 198 | c[i] = 0 199 | for i in range(1, nc + 1): 200 | c[i] = c[i - 1] 201 | for j in range(i - 1, 0, -1): 202 | c[j] = c[j - 1] ^ prod(c[j], alog[i], log, alog, gf) 203 | c[0] = prod(c[0], alog[i], log, alog, gf) 204 | # generate codewords 205 | for i in range(nd, nd + nc): 206 | wd[i] = 0 207 | for i in range(nd): 208 | assert 0 <= wd[i] < gf 209 | k = wd[nd] ^ wd[i] 210 | for j in range(nc): 211 | wd[nd + j] = prod(k, c[nc - j - 1], log, alog, gf) 212 | if j < nc - 1: 213 | wd[nd + j] ^= wd[nd + j + 1] 214 | 215 | 216 | def find_optimal_sequence(data, encoding=None): 217 | """ Find optimal sequence, i.e. with minimum number of bits to encode data. 218 | 219 | TODO: add support of FLG(n) processing 220 | 221 | :param data: string or bytes to encode 222 | :param encoding: see :py:class:`AztecCode` 223 | :return: optimal sequence 224 | """ 225 | 226 | # standardize encoding name, ensure that it's valid for ECI, and encode string to bytes 227 | if encoding: 228 | encoding = codecs.lookup(encoding).name 229 | eci = encoding_to_eci[encoding] 230 | else: 231 | encoding = 'iso8859-1' 232 | eci = None 233 | if isinstance(data, str): 234 | data = data.encode(encoding) 235 | 236 | back_to = {m: Mode.UPPER for m in Mode} 237 | cur_len = {m: 0 if m==Mode.UPPER else E for m in Mode} 238 | cur_seq = {m: [] for m in Mode} 239 | prev_c = None 240 | for c in data: 241 | for x in Mode: 242 | for y in Mode: 243 | if cur_len[x] + latch_len[x][y] < cur_len[y]: 244 | cur_len[y] = cur_len[x] + latch_len[x][y] 245 | cur_seq[y] = cur_seq[x][:] 246 | back_to[y] = y 247 | if y == Mode.BINARY: 248 | # for binary mode use B/S instead of B/L 249 | if x in (Mode.PUNCT, Mode.DIGIT): 250 | # if changing from punct or digit to binary mode use U/L as intermediate mode 251 | # TODO: update for digit 252 | back_to[y] = Mode.UPPER 253 | cur_seq[y] += [Latch.UPPER, Shift.BINARY, Misc.SIZE] 254 | else: 255 | back_to[y] = x 256 | cur_seq[y] += [Shift.BINARY, Misc.SIZE] 257 | elif cur_seq[x]: 258 | # if changing from punct or digit mode - use U/L as intermediate mode 259 | # TODO: update for digit 260 | if x == Mode.DIGIT and y == Mode.PUNCT: 261 | cur_seq[y] += [Misc.RESUME, Latch.UPPER, Latch.MIXED, Latch.PUNCT] 262 | elif x in (Mode.PUNCT, Mode.DIGIT) and y != Mode.UPPER: 263 | cur_seq[y] += [Misc.RESUME, Latch.UPPER, Latch[y.name]] 264 | elif x == Mode.LOWER and y == Mode.UPPER: 265 | cur_seq[y] += [Latch.DIGIT, Latch.UPPER] 266 | elif x in (Mode.UPPER, Mode.LOWER) and y == Mode.PUNCT: 267 | cur_seq[y] += [Latch.MIXED, Latch[y.name]] 268 | elif x == Mode.MIXED and y != Mode.UPPER: 269 | if y == Mode.PUNCT: 270 | cur_seq[y] += [Latch.PUNCT] 271 | back_to[y] = Mode.PUNCT 272 | else: 273 | cur_seq[y] += [Latch.UPPER, Latch.DIGIT] 274 | back_to[y] = Mode.DIGIT 275 | continue 276 | elif x == Mode.BINARY: 277 | # TODO: review this 278 | # Reviewed by jravallec 279 | if y == back_to[x]: 280 | # when return from binary to previous mode, skip mode change 281 | cur_seq[y] += [Misc.RESUME] 282 | elif y == Mode.UPPER: 283 | if back_to[x] == Mode.LOWER: 284 | cur_seq[y] += [Misc.RESUME, Latch.DIGIT, Latch.UPPER] 285 | if back_to[x] == Mode.MIXED: 286 | cur_seq[y] += [Misc.RESUME, Latch.UPPER] 287 | elif y == Mode.LOWER: 288 | cur_seq[y] += [Misc.RESUME, Latch.LOWER] 289 | elif y == Mode.MIXED: 290 | cur_seq[y] += [Misc.RESUME, Latch.MIXED] 291 | elif y == Mode.PUNCT: 292 | if back_to[x] == Mode.MIXED: 293 | cur_seq[y] += [Misc.RESUME, Latch.PUNCT] 294 | else: 295 | cur_seq[y] += [Misc.RESUME, Latch.MIXED, Latch.PUNCT] 296 | elif y == Mode.DIGIT: 297 | if back_to[x] == Mode.MIXED: 298 | cur_seq[y] += [Misc.RESUME, Latch.UPPER, Latch.DIGIT] 299 | else: 300 | cur_seq[y] += [Misc.RESUME, Latch.DIGIT] 301 | else: 302 | cur_seq[y] += [Misc.RESUME, Latch[y.name]] 303 | else: 304 | # if changing from punct or digit mode - use U/L as intermediate mode 305 | # TODO: update for digit 306 | if x in (Mode.PUNCT, Mode.DIGIT): 307 | cur_seq[y] = [Latch.UPPER, Latch[y.name]] 308 | elif x == Mode.LOWER and y == Mode.UPPER: 309 | cur_seq[y] = [Latch.DIGIT, Latch.UPPER] 310 | elif x in (Mode.BINARY, Mode.UPPER, Mode.LOWER) and y == Mode.PUNCT: 311 | cur_seq[y] = [Latch.MIXED, Latch[y.name]] 312 | else: 313 | cur_seq[y] = [Latch[y.name]] 314 | next_len = {m:E for m in Mode} 315 | next_seq = {m:[] for m in Mode} 316 | possible_modes = [m for m in Mode if m == Mode.BINARY or c in code_chars[m]] 317 | for x in possible_modes: 318 | # TODO: review this! 319 | if back_to[x] == Mode.DIGIT and x == Mode.LOWER: 320 | cur_seq[x] += [Latch.UPPER, Latch.LOWER] 321 | cur_len[x] += latch_len[back_to[x]][x] 322 | back_to[x] = Mode.LOWER 323 | # add char to current sequence 324 | if cur_len[x] + char_size[x] < next_len[x]: 325 | next_len[x] = cur_len[x] + char_size[x] 326 | next_seq[x] = cur_seq[x] + [c] 327 | for y in Mode: 328 | if (y, x) in shift_len and cur_len[y] + shift_len[(y, x)] + char_size[x] < next_len[y]: 329 | next_len[y] = cur_len[y] + shift_len[y, x] + char_size[x] 330 | next_seq[y] = cur_seq[y] + [Shift[x.name], c] 331 | # TODO: review this!!! 332 | if prev_c and bytes((prev_c, c)) in punct_2_chars: 333 | for x in Mode: 334 | # Will never StopIteration because we must have one S/L already since prev_c is PUNCT 335 | last_mode = next(s.value for s in reversed(cur_seq[x]) if isinstance(s, Latch) or isinstance(s, Shift)) 336 | if last_mode == Mode.PUNCT: 337 | last_c = cur_seq[x][-1] 338 | if isinstance(last_c, int) and bytes((last_c, c)) in punct_2_chars: 339 | if x != Mode.MIXED: # we need to avoid this because it contains '\r', '\n' individually, but not combined 340 | if cur_len[x] < next_len[x]: 341 | next_len[x] = cur_len[x] 342 | next_seq[x] = cur_seq[x][:-1] + [ bytes((last_c, c)) ] 343 | if len(next_seq[Mode.BINARY]) - 2 == 32: 344 | next_len[Mode.BINARY] += 11 345 | cur_len = next_len.copy() 346 | cur_seq = next_seq.copy() 347 | prev_c = c 348 | # sort in ascending order and get shortest sequence 349 | result_seq = [] 350 | sorted_cur_len = sorted(cur_len, key=cur_len.__getitem__) 351 | if sorted_cur_len: 352 | min_length = sorted_cur_len[0] 353 | result_seq = cur_seq[min_length] 354 | # update binary sequences' sizes 355 | sizes = {} 356 | result_seq_len = len(result_seq) 357 | reset_pos = result_seq_len - 1 358 | for i, c in enumerate(reversed(result_seq)): 359 | if c == Misc.SIZE: 360 | sizes[i] = reset_pos - (result_seq_len - i - 1) 361 | reset_pos = result_seq_len - i 362 | elif c == Misc.RESUME: 363 | reset_pos = result_seq_len - i - 2 364 | for size_pos in sizes: 365 | result_seq[len(result_seq) - size_pos - 1] = sizes[size_pos] 366 | # remove 'resume' tokens 367 | result_seq = [x for x in result_seq if x != Misc.RESUME] 368 | # update binary sequences' extra sizes 369 | updated_result_seq = [] 370 | is_binary_length = False 371 | for i, c in enumerate(result_seq): 372 | if is_binary_length: 373 | if c > 31: 374 | updated_result_seq.append(0) 375 | updated_result_seq.append(c - 31) 376 | else: 377 | updated_result_seq.append(c) 378 | is_binary_length = False 379 | else: 380 | updated_result_seq.append(c) 381 | 382 | if c == Shift.BINARY: 383 | is_binary_length = True 384 | 385 | if eci is not None: 386 | updated_result_seq = [ Shift.PUNCT, Misc.FLG, len(str(eci)), eci ] + updated_result_seq 387 | 388 | return updated_result_seq 389 | 390 | 391 | def optimal_sequence_to_bits(optimal_sequence): 392 | """ Convert optimal sequence to bits 393 | 394 | :param optimal_sequence: input optimal sequence 395 | :return: string with bits 396 | """ 397 | out_bits = '' 398 | mode = prev_mode = Mode.UPPER 399 | shift = False 400 | sequence = optimal_sequence[:] 401 | while sequence: 402 | # read one item from sequence 403 | ch = sequence.pop(0) 404 | index = code_chars[mode].index(ch) 405 | out_bits += bin(index)[2:].zfill(char_size[mode]) 406 | # resume previous mode for shift 407 | if shift: 408 | mode = prev_mode 409 | shift = False 410 | # get mode from sequence character 411 | if isinstance(ch, Latch): 412 | mode = ch.value 413 | # handle FLG(n) 414 | elif ch == Misc.FLG: 415 | if not sequence: 416 | raise Exception('Expected FLG(n) value') 417 | flg_n = sequence.pop(0) 418 | if not isinstance(flg_n, numbers.Number) or not 0 <= flg_n <= 7: 419 | raise Exception('FLG(n) value must be a number from 0 to 7') 420 | if flg_n == 7: 421 | raise Exception('FLG(7) is reserved and currently illegal') 422 | 423 | out_bits += bin(flg_n)[2:].zfill(3) 424 | if flg_n >= 1: 425 | # ECI 426 | if not sequence: 427 | raise Exception('Expected FLG({}) to be followed by ECI code'.format(flg_n)) 428 | eci_code = sequence.pop(0) 429 | if not isinstance(eci_code, numbers.Number) or not 0 <= eci_code < (10**flg_n): 430 | raise Exception('Expected FLG({}) ECI code to be a number from 0 to {}'.format(flg_n, (10**flg_n) - 1)) 431 | out_digits = str(eci_code).zfill(flg_n).encode() 432 | for ch in out_digits: 433 | index = code_chars[Mode.DIGIT].index(ch) 434 | out_bits += bin(index)[2:].zfill(char_size[Mode.DIGIT]) 435 | # handle binary run 436 | elif ch == Shift.BINARY: 437 | if not sequence: 438 | raise Exception('Expected binary sequence length') 439 | # followed by a 5 bit length 440 | seq_len = sequence.pop(0) 441 | if not isinstance(seq_len, numbers.Number): 442 | raise Exception('Binary sequence length must be a number') 443 | out_bits += bin(seq_len)[2:].zfill(5) 444 | # if length is zero - 11 additional length bits are used for length 445 | if not seq_len: 446 | seq_len = sequence.pop(0) 447 | if not isinstance(seq_len, numbers.Number): 448 | raise Exception('Binary sequence length must be a number') 449 | out_bits += bin(seq_len)[2:].zfill(11) 450 | seq_len += 31 451 | for binary_index in range(seq_len): 452 | ch = sequence.pop(0) 453 | out_bits += bin(ch)[2:].zfill(char_size[Mode.BINARY]) 454 | # handle other shift 455 | elif isinstance(ch, Shift): 456 | mode, prev_mode = ch.value, mode 457 | shift = True 458 | return out_bits 459 | 460 | 461 | def get_data_codewords(bits, codeword_size): 462 | """ Get codewords stream from data bits sequence 463 | Bit stuffing and padding are used to avoid all-zero and all-ones codewords 464 | 465 | :param bits: input data bits 466 | :param codeword_size: codeword size in bits 467 | :return: data codewords 468 | """ 469 | codewords = [] 470 | sub_bits = '' 471 | for bit in bits: 472 | sub_bits += bit 473 | # if first bits of sub sequence are zeros add 1 as a last bit 474 | if len(sub_bits) == codeword_size - 1 and sub_bits.find('1') < 0: 475 | sub_bits += '1' 476 | # if first bits of sub sequence are ones add 0 as a last bit 477 | if len(sub_bits) == codeword_size - 1 and sub_bits.find('0') < 0: 478 | sub_bits += '0' 479 | # convert bits to decimal int and add to result codewords 480 | if len(sub_bits) >= codeword_size: 481 | codewords.append(int(sub_bits, 2)) 482 | sub_bits = '' 483 | if sub_bits: 484 | # update and add final bits 485 | sub_bits = sub_bits.ljust(codeword_size, '1') 486 | # change final bit to zero if all bits are ones 487 | if sub_bits.find('0') < 0: 488 | sub_bits = sub_bits[:-1] + '0' 489 | codewords.append(int(sub_bits, 2)) 490 | return codewords 491 | 492 | 493 | def get_config_from_table(size, compact): 494 | """ Get config with given size and compactness flag 495 | 496 | :param size: matrix size 497 | :param compact: compactness flag 498 | :return: dict with config 499 | """ 500 | try: 501 | return configs[(size, compact)] 502 | except KeyError: 503 | raise NotImplementedError('Failed to find config with size and compactness flag') 504 | 505 | def find_suitable_matrix_size(data, ec_percent=23, encoding=None): 506 | """ Find suitable matrix size 507 | Raise an exception if suitable size is not found 508 | 509 | :param data: string or bytes to encode 510 | :param ec_percent: percentage of symbol capacity for error correction (default 23%) 511 | :param encoding: see :py:class:`AztecCode` 512 | :return: (size, compact) tuple 513 | """ 514 | optimal_sequence = find_optimal_sequence(data, encoding) 515 | out_bits = optimal_sequence_to_bits(optimal_sequence) 516 | for (size, compact) in sorted(configs.keys()): 517 | config = get_config_from_table(size, compact) 518 | bits = config.bits 519 | # calculate minimum required number of bits 520 | required_bits_count = int(math.ceil((len(out_bits) + 3) * 100.0 / (100 - ec_percent))) 521 | if required_bits_count < bits: 522 | return size, compact, optimal_sequence 523 | raise Exception('Data too big to fit in one Aztec code!') 524 | 525 | class AztecCode(object): 526 | """ 527 | Aztec code generator 528 | """ 529 | 530 | def __init__(self, data, size=None, compact=None, ec_percent=23, encoding=None): 531 | """ Create Aztec code with given data. 532 | If size and compact parameters are None (by default), an 533 | optimal size and compactness calculated based on the data. 534 | 535 | :param data: string or bytes to encode 536 | :param size: size of matrix 537 | :param compact: compactness flag 538 | :param ec_percent: percentage of symbol capacity for error correction (default 23%) 539 | :param encoding: 540 | If set, sequence will include an initial ECI mark corresponding to the specified encoding (see :py:mod:`codecs`) 541 | If unset, no ECI mark will be included and string must be encodable as 'iso8859-1' 542 | """ 543 | self.data = data 544 | self.encoding = encoding 545 | self.sequence = None 546 | self.ec_percent = ec_percent 547 | if size is not None and compact is not None: 548 | if (size, compact) in configs: 549 | self.size, self.compact = size, compact 550 | else: 551 | raise Exception( 552 | 'Given size and compact values (%s, %s) are not found in sizes table!' % (size, compact)) 553 | else: 554 | self.size, self.compact, self.sequence = find_suitable_matrix_size(self.data, ec_percent, encoding) 555 | self.__create_matrix() 556 | self.__encode_data() 557 | 558 | def __create_matrix(self): 559 | """ Create Aztec code matrix with given size """ 560 | self.matrix = [array.array('B', (0 for jj in range(self.size))) for ii in range(self.size)] 561 | 562 | def save(self, filename, module_size=2, border=0, format=None): 563 | """ Save matrix to image file 564 | 565 | :param filename: output image filename (or file object, with format). 566 | :param module_size: barcode module size in pixels. 567 | :param border: barcode border size in modules. 568 | :param format: Pillow image format, such as 'PNG' 569 | """ 570 | self.image(module_size, border).save(filename, format=format) 571 | 572 | def image(self, module_size=2, border=0): 573 | """ Create PIL image 574 | 575 | :param module_size: barcode module size in pixels. 576 | :param border: barcode border size in modules 577 | """ 578 | if ImageDraw is None: 579 | exc = missing_pil[0](missing_pil[1]) 580 | exc.__traceback__ = missing_pil[2] 581 | raise exc 582 | image = Image.new('1', ((self.size+2*border) * module_size, (self.size+2*border) * module_size), 1) 583 | image_draw = ImageDraw.Draw(image) 584 | for y in range(self.size): 585 | for x in range(self.size): 586 | image_draw.rectangle( 587 | ((x+border) * module_size, (y+border) * module_size, 588 | (x+border+1) * module_size, (y+border+1) * module_size), 589 | fill=not self.matrix[y][x]) 590 | return image 591 | 592 | def print_out(self, border=0): 593 | """ Print out Aztec code matrix using ASCII output """ 594 | print('\n'.join(' '*(2*border + self.size) for ii in range(border))) 595 | for line in self.matrix: 596 | print(' '*border + ''.join(('#' if x else ' ') for x in line) + ' '*border) 597 | print('\n'.join(' '*(2*border + self.size) for ii in range(border))) 598 | 599 | def print_fancy(self, border=0): 600 | """ Print out Aztec code matrix using Unicode box-drawing characters and ANSI colorization """ 601 | for y in range(-border, self.size+border, 2): 602 | last_half_row = (y==self.size + border - 1) 603 | ul = '\x1b[40;37;1m' + ('\u2580' if last_half_row else '\u2588')*border 604 | for x in range(0, self.size): 605 | a = self.matrix[y][x] if 0 <= y < self.size else None 606 | b = self.matrix[y+1][x] if -1 <= y < self.size-1 else last_half_row 607 | ul += ' ' if a and b else '\u2584' if a else '\u2580' if b else '\u2588' 608 | ul += ('\u2580' if last_half_row else '\u2588')*border + '\x1b[0m' 609 | print(ul) 610 | 611 | def __add_finder_pattern(self): 612 | """ Add bulls-eye finder pattern """ 613 | center = self.size // 2 614 | ring_radius = 5 if self.compact else 7 615 | for x in range(-ring_radius, ring_radius): 616 | for y in range(-ring_radius, ring_radius): 617 | self.matrix[center + y][center + x] = (max(abs(x), abs(y)) + 1) % 2 618 | 619 | def __add_orientation_marks(self): 620 | """ Add orientation marks to matrix """ 621 | center = self.size // 2 622 | ring_radius = 5 if self.compact else 7 623 | # add orientation marks 624 | # left-top 625 | self.matrix[center - ring_radius][center - ring_radius] = 1 626 | self.matrix[center - ring_radius + 1][center - ring_radius] = 1 627 | self.matrix[center - ring_radius][center - ring_radius + 1] = 1 628 | # right-top 629 | self.matrix[center - ring_radius + 0][center + ring_radius + 0] = 1 630 | self.matrix[center - ring_radius + 1][center + ring_radius + 0] = 1 631 | # right-down 632 | self.matrix[center + ring_radius - 1][center + ring_radius + 0] = 1 633 | 634 | def __add_reference_grid(self): 635 | """ Add reference grid to matrix """ 636 | if self.compact: 637 | return 638 | center = self.size // 2 639 | ring_radius = 5 if self.compact else 7 640 | for x in range(-center, center + 1): 641 | for y in range(-center, center + 1): 642 | # skip finder pattern 643 | if -ring_radius <= x <= ring_radius and -ring_radius <= y <= ring_radius: 644 | continue 645 | # set pixel 646 | if x % 16 == 0 or y % 16 == 0: 647 | self.matrix[center + y][center + x] = (x + y + 1) % 2 648 | 649 | def __get_mode_message(self, layers_count, data_cw_count): 650 | """ Get mode message 651 | 652 | :param layers_count: number of layers 653 | :param data_cw_count: number of data codewords 654 | :return: mode message codewords 655 | """ 656 | if self.compact: 657 | # for compact mode - 2 bits with layers count and 6 bits with data codewords count 658 | mode_word = '{0:02b}{1:06b}'.format(layers_count - 1, data_cw_count - 1) 659 | # two 4 bits initial codewords with 5 Reed-Solomon check codewords 660 | init_codewords = [int(mode_word[i:i + 4], 2) for i in range(0, 8, 4)] 661 | total_cw_count = 7 662 | else: 663 | # for full mode - 5 bits with layers count and 11 bits with data codewords count 664 | mode_word = '{0:05b}{1:011b}'.format(layers_count - 1, data_cw_count - 1) 665 | # four 4 bits initial codewords with 6 Reed-Solomon check codewords 666 | init_codewords = [int(mode_word[i:i + 4], 2) for i in range(0, 16, 4)] 667 | total_cw_count = 10 668 | # fill Reed-Solomon check codewords with zeros 669 | init_cw_count = len(init_codewords) 670 | codewords = (init_codewords + [0] * (total_cw_count - init_cw_count))[:total_cw_count] 671 | # update Reed-Solomon check codewords using GF(16) 672 | reed_solomon(codewords, init_cw_count, total_cw_count - init_cw_count, 16, polynomials[4]) 673 | return codewords 674 | 675 | def __add_mode_info(self, data_cw_count): 676 | """ Add mode info to matrix 677 | 678 | :param data_cw_count: number of data codewords. 679 | """ 680 | config = get_config_from_table(self.size, self.compact) 681 | layers_count = config.layers 682 | mode_data_values = self.__get_mode_message(layers_count, data_cw_count) 683 | mode_data_bits = ''.join('{0:04b}'.format(v) for v in mode_data_values) 684 | 685 | center = self.size // 2 686 | ring_radius = 5 if self.compact else 7 687 | side_size = 7 if self.compact else 11 688 | bits_stream = StringIO(mode_data_bits) 689 | x = 0 690 | y = 0 691 | index = 0 692 | while True: 693 | # for full mode take a reference grid into account 694 | if not self.compact: 695 | if (index % side_size) == 5: 696 | index += 1 697 | continue 698 | # read one bit 699 | bit = bits_stream.read(1) 700 | if not bit: 701 | break 702 | if 0 <= index < side_size: 703 | # top 704 | x = index + 2 - ring_radius 705 | y = -ring_radius 706 | elif side_size <= index < side_size * 2: 707 | # right 708 | x = ring_radius 709 | y = index % side_size + 2 - ring_radius 710 | elif side_size * 2 <= index < side_size * 3: 711 | # bottom 712 | x = ring_radius - index % side_size - 2 713 | y = ring_radius 714 | elif side_size * 3 <= index < side_size * 4: 715 | # left 716 | x = -ring_radius 717 | y = ring_radius - index % side_size - 2 718 | # set pixel 719 | self.matrix[center + y][center + x] = (bit == '1') 720 | index += 1 721 | 722 | def __add_data(self, data, encoding): 723 | """ Add data to encode to the matrix 724 | 725 | :param data: data to encode 726 | :param encoding: see :py:class:`AztecCode` 727 | :return: number of data codewords 728 | """ 729 | if not self.sequence: 730 | self.sequence = find_optimal_sequence(data, encoding) 731 | out_bits = optimal_sequence_to_bits(self.sequence) 732 | config = get_config_from_table(self.size, self.compact) 733 | layers_count = config.layers 734 | cw_count = config.codewords 735 | cw_bits = config.cw_bits 736 | bits = config.bits 737 | 738 | # calculate minimum required number of bits 739 | required_bits_count = int(math.ceil((len(out_bits) + 3) * 100.0 / (100 - self.ec_percent))) 740 | data_codewords = get_data_codewords(out_bits, cw_bits) 741 | if required_bits_count > bits: 742 | raise Exception('Data too big to fit in Aztec code with current size!') 743 | 744 | # add Reed-Solomon codewords to init data codewords 745 | data_cw_count = len(data_codewords) 746 | codewords = (data_codewords + [0] * (cw_count - data_cw_count))[:cw_count] 747 | reed_solomon(codewords, data_cw_count, cw_count - data_cw_count, 2 ** cw_bits, polynomials[cw_bits]) 748 | 749 | center = self.size // 2 750 | ring_radius = 5 if self.compact else 7 751 | 752 | num = 2 753 | side = Side.top 754 | layer_index = 0 755 | pos_x = center - ring_radius 756 | pos_y = center - ring_radius - 1 757 | full_bits = ''.join(bin(cw)[2:].zfill(cw_bits) for cw in codewords)[::-1] 758 | for i in range(0, len(full_bits), 2): 759 | num += 1 760 | max_num = ring_radius * 2 + layer_index * 4 + (4 if self.compact else 3) 761 | bits_pair = [(bit == '1') for bit in full_bits[i:i + 2]] 762 | if layer_index >= layers_count: 763 | raise Exception('Maximum layer count for current size is exceeded!') 764 | if side == Side.top: 765 | # move right 766 | dy0 = 1 if not self.compact and (center - pos_y) % 16 == 0 else 0 767 | dy1 = 2 if not self.compact and (center - pos_y + 1) % 16 == 0 else 1 768 | self.matrix[pos_y - dy0][pos_x] = bits_pair[0] 769 | self.matrix[pos_y - dy1][pos_x] = bits_pair[1] 770 | pos_x += 1 771 | if num > max_num: 772 | num = 2 773 | side = Side.right 774 | pos_x -= 1 775 | pos_y += 1 776 | # skip reference grid 777 | if not self.compact and (center - pos_x) % 16 == 0: 778 | pos_x += 1 779 | if not self.compact and (center - pos_y) % 16 == 0: 780 | pos_y += 1 781 | elif side == Side.right: 782 | # move down 783 | dx0 = 1 if not self.compact and (center - pos_x) % 16 == 0 else 0 784 | dx1 = 2 if not self.compact and (center - pos_x + 1) % 16 == 0 else 1 785 | self.matrix[pos_y][pos_x - dx0] = bits_pair[1] 786 | self.matrix[pos_y][pos_x - dx1] = bits_pair[0] 787 | pos_y += 1 788 | if num > max_num: 789 | num = 2 790 | side = Side.bottom 791 | pos_x -= 2 792 | if not self.compact and (center - pos_x - 1) % 16 == 0: 793 | pos_x -= 1 794 | pos_y -= 1 795 | # skip reference grid 796 | if not self.compact and (center - pos_y) % 16 == 0: 797 | pos_y += 1 798 | if not self.compact and (center - pos_x) % 16 == 0: 799 | pos_x -= 1 800 | elif side == Side.bottom: 801 | # move left 802 | dy0 = 1 if not self.compact and (center - pos_y) % 16 == 0 else 0 803 | dy1 = 2 if not self.compact and (center - pos_y + 1) % 16 == 0 else 1 804 | self.matrix[pos_y - dy0][pos_x] = bits_pair[1] 805 | self.matrix[pos_y - dy1][pos_x] = bits_pair[0] 806 | pos_x -= 1 807 | if num > max_num: 808 | num = 2 809 | side = Side.left 810 | pos_x += 1 811 | pos_y -= 2 812 | if not self.compact and (center - pos_y - 1) % 16 == 0: 813 | pos_y -= 1 814 | # skip reference grid 815 | if not self.compact and (center - pos_x) % 16 == 0: 816 | pos_x -= 1 817 | if not self.compact and (center - pos_y) % 16 == 0: 818 | pos_y -= 1 819 | elif side == Side.left: 820 | # move up 821 | dx0 = 1 if not self.compact and (center - pos_x) % 16 == 0 else 0 822 | dx1 = 2 if not self.compact and (center - pos_x - 1) % 16 == 0 else 1 823 | self.matrix[pos_y][pos_x + dx1] = bits_pair[0] 824 | self.matrix[pos_y][pos_x + dx0] = bits_pair[1] 825 | pos_y -= 1 826 | if num > max_num: 827 | num = 2 828 | side = Side.top 829 | layer_index += 1 830 | # skip reference grid 831 | if not self.compact and (center - pos_y) % 16 == 0: 832 | pos_y -= 1 833 | return data_cw_count 834 | 835 | def __encode_data(self): 836 | """ Encode data """ 837 | self.__add_finder_pattern() 838 | self.__add_orientation_marks() 839 | self.__add_reference_grid() 840 | data_cw_count = self.__add_data(self.data, self.encoding) 841 | self.__add_mode_info(data_cw_count) 842 | 843 | 844 | def main(argv): 845 | if len(argv) not in (2, 3): 846 | print("usage: {} STRING_TO_ENCODE [IMAGE_FILE]".format(argv[0])) 847 | print(" Generate a 2D Aztec barcode and print it, or save to a file.") 848 | raise SystemExit(1) 849 | data = argv[1] 850 | aztec_code = AztecCode(data) 851 | print('Aztec Code info: {0}x{0} {1}'.format(aztec_code.size, '(compact)' if aztec_code.compact else '')) 852 | if len(sys.argv) == 3: 853 | aztec_code.save(argv[2], module_size=5) 854 | else: 855 | aztec_code.print_fancy(border=2) 856 | 857 | 858 | if __name__ == '__main__': 859 | main(sys.argv) 860 | --------------------------------------------------------------------------------