Skip to content
Closed
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
131 changes: 73 additions & 58 deletions aztec_code_generator.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""
aztec_code_generator
~~~~~~~~~~~~~~~~~~~~

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 @@ -25,7 +25,6 @@
except ImportError:
from io import StringIO


table = {
(15, True): {'layers': 1, 'codewords': 17, 'cw_bits': 6, 'bits': 102, 'digits': 13, 'text': 12, 'bytes': 6},
(19, False): {'layers': 1, 'codewords': 21, 'cw_bits': 6, 'bits': 126, 'digits': 18, 'text': 15, 'bytes': 8},
Expand Down Expand Up @@ -160,26 +159,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 @@ -189,7 +190,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 All @@ -211,12 +212,13 @@ def reed_solomon(wd, nd, nc, gf, pp):


def find_optimal_sequence(data):
""" Find optimal sequence, i.e. with minimum number of bits to encode data.
"""Find optimal sequence, i.e. with minimum number of bits to encode data.

TODO: add support of FLG(n) processing

:param data: data to encode
:return: optimal sequence
:param list[str|int] data: Data to encode.

:return: Optimal sequence.
"""
back_to = {
'upper': 'upper', 'lower': 'upper', 'mixed': 'upper',
Expand Down Expand Up @@ -329,7 +331,7 @@ def find_optimal_sequence(data):
for x in possible_modes:
# TODO: review this!
if back_to[x] == 'digit' and x == 'lower':
cur_seq[x] = cur_seq[x] + ['U/L', 'L/L']
cur_seq[x] = cur_seq[x] + ['U/L', 'L/L']
cur_len[x] = cur_len[x] + latch_len[back_to[x]][x]
back_to[x] = 'lower'
# add char to current sequence
Expand All @@ -351,7 +353,8 @@ def find_optimal_sequence(data):
last_mode = abbr_modes.get(char.replace('/S', '').replace('/L', ''))
break
if last_mode == 'punct':
if cur_seq[x][-1] + c in punct_2_chars:
# do not use mixed mode for '\r\n' as in mixed mode '\r' and '\n' are separate
if cur_seq[x][-1] + c in punct_2_chars and x != 'mixed':
if cur_len[x] < next_len[x]:
next_len[x] = cur_len[x]
next_seq[x] = cur_seq[x][:-1] + [cur_seq[x][-1] + c]
Expand Down Expand Up @@ -397,15 +400,16 @@ def find_optimal_sequence(data):

if c == 'B/S':
is_binary_length = True

return updated_result_seq


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 = 'upper'
Expand Down Expand Up @@ -466,12 +470,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 All @@ -498,23 +503,26 @@ def get_data_codewords(bits, codeword_size):


def get_config_from_table(size, compact):
""" Get config from table with given size and compactness flag
"""Get config from table with given size and compactness flag.

:param size: matrix size
:param compact: compactness flag
:return: dict with config
:param int size: Matrix size.
:param bool compact: Compactness flag.

:return: Dict with config.
"""
config = table.get((size, compact))
if not config:
raise Exception('Failed to find config with size and compactness flag')
return config


def find_suitable_matrix_size(data):
""" Find suitable matrix size
Raise an exception if suitable size is not found
"""Find suitable matrix size.
Raise an exception if suitable size is not found.

:param list[str|int] data: Data to encode.

:param data: data to encode
:return: (size, compact) tuple
:return: (size, compact) tuple.
"""
optimal_sequence = find_optimal_sequence(data)
out_bits = optimal_sequence_to_bits(optimal_sequence)
Expand All @@ -525,24 +533,25 @@ def find_suitable_matrix_size(data):
ec_percent = 23 # recommended: 23% of symbol capacity plus 3 codewords
# calculate minimum required number of bits
required_bits_count = int(math.ceil(len(out_bits) * 100.0 / (
100 - ec_percent) + 3 * 100.0 / (100 - ec_percent)))
100 - ec_percent) + 3 * 100.0 / (100 - ec_percent)))
if required_bits_count < bits:
return size, compact
raise Exception('Data too big to fit in one Aztec code!')


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

def __init__(self, data, size=None, compact=None):
""" Create Aztec code with given data.
"""Create Aztec code with given data.
If size and compact parameters are None (by default), an
optimal size and compactness calculated based on the data.

:param data: data to encode
:param size: size of matrix
:param compact: compactness flag
:param data: Data to encode.
:param int|None size: Size of matrix.
:param bool|None compact: Compactness flag.
"""
self.data = data
if size is not None and compact is not None:
Expand All @@ -557,7 +566,7 @@ def __init__(self, data, size=None, compact=None):
self.__encode_data()

def __create_matrix(self):
""" Create Aztec code matrix with given size """
"""Create Aztec code matrix with given size."""
self.matrix = []
for _ in range(self.size):
line = []
Expand All @@ -566,10 +575,12 @@ def __create_matrix(self):
self.matrix.append(line)

def save(self, filename, module_size=1):
""" Save matrix to image file
"""Save matrix to image file.

:param str filename: Output image filename.
:param int module_size: Barcode module size in pixels.

:param filename: output image filename.
:param module_size: barcode module size in pixels.
:return: None.
"""
if ImageDraw is None:
exc = missing_pil[0](missing_pil[1])
Expand All @@ -586,12 +597,12 @@ def save(self, filename, module_size=1):
image.save(filename)

def print_out(self):
""" Print out Aztec code matrix """
"""Print out Aztec code matrix."""
for line in self.matrix:
print(''.join(x for x in line))

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):
Expand All @@ -600,7 +611,7 @@ def __add_finder_pattern(self):
self.matrix[center + y][center + x] = '#'

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 @@ -615,7 +626,7 @@ def __add_orientation_marks(self):
self.matrix[center + ring_radius - 1][center + ring_radius + 0] = '#'

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 @@ -631,11 +642,12 @@ def __add_reference_grid(self):
self.matrix[center + y][center + x] = val

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 @@ -657,9 +669,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.get('layers')
Expand Down Expand Up @@ -704,10 +718,11 @@ def __add_mode_info(self, data_cw_count):
index += 1

def __add_data(self, data):
""" Add data to encode to the matrix
"""Add data to encode to the matrix.

:param list[str|int] data: data to encode.

:param data: data to encode
:return: number of data codewords
:return: number of data codewords.
"""
optimal_sequence = find_optimal_sequence(data)
out_bits = optimal_sequence_to_bits(optimal_sequence)
Expand All @@ -721,7 +736,7 @@ def __add_data(self, data):
ec_percent = 23 # recommended
# calculate minimum required number of bits
required_bits_count = int(math.ceil(len(out_bits) * 100.0 / (
100 - ec_percent) + 3 * 100.0 / (100 - ec_percent)))
100 - ec_percent) + 3 * 100.0 / (100 - ec_percent)))
data_codewords = get_data_codewords(out_bits, cw_bits)
if required_bits_count > bits:
raise Exception('Data too big to fit in Aztec code with current size!')
Expand Down Expand Up @@ -818,7 +833,7 @@ def __add_data(self, data):
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
9 changes: 4 additions & 5 deletions test_aztec_code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_find_optimal_sequence(self):
'C', 'L/L', 'o', 'd', 'e', 'D/L', ' ', '2', 'U/S', 'D', 'P/S', '!'])
self.assertEqual(find_optimal_sequence('a\xff'), ['B/S', 2, 'a', '\xff'])
self.assertEqual(find_optimal_sequence('a' + '\xff' * 30), ['B/S', 31, 'a'] + ['\xff'] * 30)
self.assertEqual(find_optimal_sequence('a' + '\xff' * 31), ['B/S', 0, 32, 'a'] + ['\xff'] * 31)
self.assertEqual(find_optimal_sequence('a' + '\xff' * 31), ['B/S', 0, 1, 'a'] + ['\xff'] * 31)
self.assertEqual(find_optimal_sequence('!#$%&?'), ['M/L', 'P/L', '!', '#', '$', '%', '&', '?'])
self.assertEqual(find_optimal_sequence('!#$%&?\xff'), [
'M/L', 'P/L', '!', '#', '$', '%', '&', '?', 'U/L', 'B/S', 1, '\xff'])
Expand All @@ -61,12 +61,11 @@ def test_find_optimal_sequence(self):
self.assertEqual(find_optimal_sequence('ABCabc1a2b3eBC'), [
'A', 'B', 'C', 'L/L', 'a', 'b', 'c', 'B/S', 6, '1', 'a', '2', 'b', '3', 'e', 'M/L', 'U/L', 'B', 'C'])
self.assertEqual(find_optimal_sequence('0a|5Tf.l'), [
'D/L', '0', 'U/L', 'L/L', 'a', 'M/L', '|', 'U/L',
'D/L', '5', 'U/L', 'L/L', 'U/S', 'T', 'f', 'P/S', '.', 'l'])
'B/S', 5, '0', 'a', '|', '5', 'T', 'L/L', 'f', 'P/S', '.', 'l'])
self.assertEqual(find_optimal_sequence('*V1\x0c {Pa'), [
'P/S', '*', 'V', 'B/S', 2, '1', '\x0c', ' ', 'P/S', '{', 'P', 'L/L', 'a'])
'P/S', '*', 'V', 'B/S', 5, '1', '\x0c', ' ', '{', 'P', 'L/L', 'a'])
self.assertEqual(find_optimal_sequence('~Fxlb"I4'), [
'M/L', '~', 'U/L', 'D/L', 'U/S', 'F', 'U/L', 'L/L', 'x', 'l', 'b', 'D/L', 'P/S', '"', 'U/S', 'I', '4'])
'B/S', 7, '~', 'F', 'x', 'l', 'b', '"', 'I', 'D/L', '4'])
self.assertEqual(find_optimal_sequence('\\+=R?1'), [
'M/L', '\\', 'P/L', '+', '=', 'U/L', 'R', 'D/L', 'P/S', '?', '1'])

Expand Down