Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test_and_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -40,7 +40,7 @@ jobs:
needs: build
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v2
Expand All @@ -54,7 +54,7 @@ jobs:
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
- name: Test
run: |
python setup.py test
py.test --verbose

# https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml
release:
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ This is a pure-Python library to generate [Aztec Code](https://en.wikipedia.org/
- `v0.11`
- fix docstrings
- change default `module_size` in image output to 2 pixels; ZXing can't read with `module_size=1`

- `v0.12`
- support for svg files in `save` method

## Installation

Expand Down Expand Up @@ -110,6 +111,8 @@ Originally written by [Dmitry Alimov (delimtry)](https://github.com/delimitry).
Updates, bug fixes, Python 3-ification, and careful `bytes`-vs.-`str` handling
by [Daniel Lenski (dlenski)](https://github.com/dlenski).

Support for SVG files by [Mateusz Bilicki (zazzik1)](https://github.com/zazzik1).

## License:

Released under [The MIT License](https://github.com/delimitry/aztec_code_generator/blob/master/LICENSE).
127 changes: 96 additions & 31 deletions aztec_code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Aztec code generator.

:copyright: (c) 2016-2018 by Dmitry Alimov.
:copyright: (c) 2016-2022 by Dmitry Alimov.
:license: The MIT License (MIT), see LICENSE for more details.
"""

Expand All @@ -17,6 +17,8 @@
import codecs
from collections import namedtuple
from enum import Enum
from io import IOBase


try:
from PIL import Image, ImageDraw
Expand Down Expand Up @@ -163,26 +165,28 @@


def prod(x, y, log, alog, gf):
""" Product x times y """
"""Product x times y."""
if not x or not y:
return 0
return alog[(log[x] + log[y]) % (gf - 1)]


def reed_solomon(wd, nd, nc, gf, pp):
""" Calculate error correction codewords
"""Calculate error correction codewords.

Algorithm is based on Aztec Code bar code symbology specification from
GOST-R-ISO-MEK-24778-2010 (Russian)
Takes ``nd`` data codeword values in ``wd`` and adds on ``nc`` check
codewords, all within GF(gf) where ``gf`` is a power of 2 and ``pp``
is the value of its prime modulus polynomial.

:param wd: data codewords (in/out param)
:param nd: number of data codewords
:param nc: number of error correction codewords
:param gf: Galois Field order
:param pp: prime modulus polynomial value
:param list[int] wd: Data codewords (in/out param).
:param int nd: Number of data codewords.
:param int nc: Number of error correction codewords.
:param int gf: Galois Field order.
:param int pp: Prime modulus polynomial value.

:return: None.
"""
# generate log and anti log tables
log = {0: 1 - gf}
Expand All @@ -192,7 +196,7 @@ def reed_solomon(wd, nd, nc, gf, pp):
if alog[i] >= gf:
alog[i] ^= pp
log[alog[i]] = i
# generate polynomial coeffs
# generate polynomial coefficients
c = {0: 1}
for i in range(1, nc + 1):
c[i] = 0
Expand Down Expand Up @@ -388,10 +392,11 @@ def find_optimal_sequence(data, encoding=None):


def optimal_sequence_to_bits(optimal_sequence):
""" Convert optimal sequence to bits
"""Convert optimal sequence to bits.

:param list[str|int] optimal_sequence: Input optimal sequence.

:param optimal_sequence: input optimal sequence
:return: string with bits
:return: String with bits.
"""
out_bits = ''
mode = prev_mode = Mode.UPPER
Expand Down Expand Up @@ -458,12 +463,13 @@ def optimal_sequence_to_bits(optimal_sequence):


def get_data_codewords(bits, codeword_size):
""" Get codewords stream from data bits sequence
Bit stuffing and padding are used to avoid all-zero and all-ones codewords
"""Get codewords stream from data bits sequence.
Bit stuffing and padding are used to avoid all-zero and all-ones codewords.

:param bits: input data bits
:param codeword_size: codeword size in bits
:return: data codewords
:param str bits: Input data bits.
:param int codeword_size: Codeword size in bits.

:return: Data codewords.
"""
codewords = []
sub_bits = ''
Expand Down Expand Up @@ -492,9 +498,7 @@ def get_data_codewords(bits, codeword_size):
def get_config_from_table(size, compact):
""" Get config with given size and compactness flag

:param size: matrix size
:param compact: compactness flag
:return: dict with config
:return: Dict with config.
"""
try:
return configs[(size, compact)]
Expand All @@ -521,9 +525,56 @@ def find_suitable_matrix_size(data, ec_percent=23, encoding=None):
return size, compact, optimal_sequence
raise Exception('Data too big to fit in one Aztec code!')


class SvgFactory:
def __init__(self, data):
""" Do not call it directly, use the create_svg method instead

:param data: String representation of the image
"""
self.svg_str = data

@staticmethod
def create_svg(matrix, border=1, matching_fn=lambda x: x == 1):
""" Creates the image in SVG format based on the two dimensional array

:param matrix: Two dimensional array of data
:param border: Border width (px)
:param matching_fn: Function to differenciate ones from zeros in the matrix
:return: An instance of SvgFactory
"""
d = ''
for y, line in enumerate(matrix):
dx = 0
x0 = None
for x, char in enumerate(line):
if matching_fn(char):
dx += 1
if x0 is None:
x0 = x
if x0 is not None and (x + 1 >= len(line) or not matching_fn(line[x + 1])):
d += f" M{x0 + border} {y + border} h{dx}"
dx = 0
x0 = None
size = len(matrix[0]) + (2 * border)
data = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}"><rect x="0" y="0" width="{size}" height="{size}" fill="white" /><path d="{d[1:]} Z" stroke="black" stroke-width="1" transform="translate(0,0.5)" /></svg>'
return SvgFactory(data)

def save(self, filename):
""" Save SVG to image file

:param filename: output image filename or file object
:return: None
"""
if isinstance(filename, IOBase) or hasattr(filename, 'write'):
return filename.write(self.svg_str)
with open(filename, 'w') as file:
file.write(self.svg_str)


class AztecCode(object):
"""
Aztec code generator
Aztec code generator.
"""

def __init__(self, data, size=None, compact=None, ec_percent=23, encoding=None):
Expand Down Expand Up @@ -558,6 +609,15 @@ def __create_matrix(self):
""" Create Aztec code matrix with given size """
self.matrix = [array.array('B', (0 for jj in range(self.size))) for ii in range(self.size)]

def __is_svg_file(self, filename, format):
""" Detects if the file is in SVG format
:param filename: image filename (or file object, with format)
:param format: image format (PNG, SVG, etc.) or None
"""
return (format is not None and format.lower() == 'svg') or (format is None and \
(isinstance(filename, str) and filename.lower().endswith('.svg')) or \
(isinstance(filename, IOBase) or hasattr(filename, 'write')) and filename.name.lower().endswith('.svg'))

def save(self, filename, module_size=2, border=0, format=None):
""" Save matrix to image file

Expand All @@ -566,6 +626,8 @@ def save(self, filename, module_size=2, border=0, format=None):
:param border: barcode border size in modules.
:param format: Pillow image format, such as 'PNG'
"""
if self.__is_svg_file(filename, format):
return SvgFactory.create_svg(self.matrix, border).save(filename)
self.image(module_size, border).save(filename, format=format)

def image(self, module_size=2, border=0):
Expand Down Expand Up @@ -608,15 +670,15 @@ def print_fancy(self, border=0):
print(ul)

def __add_finder_pattern(self):
""" Add bulls-eye finder pattern """
"""Add bulls-eye finder pattern."""
center = self.size // 2
ring_radius = 5 if self.compact else 7
for x in range(-ring_radius, ring_radius):
for y in range(-ring_radius, ring_radius):
self.matrix[center + y][center + x] = (max(abs(x), abs(y)) + 1) % 2

def __add_orientation_marks(self):
""" Add orientation marks to matrix """
"""Add orientation marks to matrix."""
center = self.size // 2
ring_radius = 5 if self.compact else 7
# add orientation marks
Expand All @@ -631,7 +693,7 @@ def __add_orientation_marks(self):
self.matrix[center + ring_radius - 1][center + ring_radius + 0] = 1

def __add_reference_grid(self):
""" Add reference grid to matrix """
"""Add reference grid to matrix."""
if self.compact:
return
center = self.size // 2
Expand All @@ -646,11 +708,12 @@ def __add_reference_grid(self):
self.matrix[center + y][center + x] = (x + y + 1) % 2

def __get_mode_message(self, layers_count, data_cw_count):
""" Get mode message
"""Get mode message.

:param layers_count: number of layers
:param data_cw_count: number of data codewords
:return: mode message codewords
:param int layers_count: Number of layers.
:param int data_cw_count: Number of data codewords.

:return: Mode message codewords.
"""
if self.compact:
# for compact mode - 2 bits with layers count and 6 bits with data codewords count
Expand All @@ -672,9 +735,11 @@ def __get_mode_message(self, layers_count, data_cw_count):
return codewords

def __add_mode_info(self, data_cw_count):
""" Add mode info to matrix
"""Add mode info to matrix.

:param int data_cw_count: Number of data codewords.

:param data_cw_count: number of data codewords.
:return: None.
"""
config = get_config_from_table(self.size, self.compact)
layers_count = config.layers
Expand Down Expand Up @@ -832,7 +897,7 @@ def __add_data(self, data, encoding):
return data_cw_count

def __encode_data(self):
""" Encode data """
"""Encode data."""
self.__add_finder_pattern()
self.__add_orientation_marks()
self.__add_reference_grid()
Expand Down
5 changes: 2 additions & 3 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pillow>=3.0,<6.0; python_version < '3.5'
pillow>=3.0,<8.0; python_version >= '3.5' and python_version < '3.6'
pillow>=8.0; python_version >= '3.6'
pytest>=8.3.3
pillow>=8.0
zxing>=0.13
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
sys.exit("Python 3.4+ is required; you are using %s" % sys.version)

setup(name="aztec_code_generator",
version="0.11",
version="0.12",
description='Aztec Code generator in Python',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
Expand Down
Loading