From 2cd313acfe84d02b117bf9dd025165b6e94a1022 Mon Sep 17 00:00:00 2001 From: Dmitry Alimov Date: Mon, 19 Oct 2020 10:47:39 +0300 Subject: [PATCH 1/4] Fix tests Apply fixes by @dlenski --- test_aztec_code_generator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test_aztec_code_generator.py b/test_aztec_code_generator.py index 72f06f8..68e78cc 100644 --- a/test_aztec_code_generator.py +++ b/test_aztec_code_generator.py @@ -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']) @@ -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']) From 120c4df93a18c112f8024f2c563d3ddcdb6b2779 Mon Sep 17 00:00:00 2001 From: Dmitry Alimov Date: Wed, 14 Sep 2022 22:55:03 +0400 Subject: [PATCH 2/4] Fix '\r\n' in mixed mode --- aztec_code_generator.py | 131 ++++++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 58 deletions(-) diff --git a/aztec_code_generator.py b/aztec_code_generator.py index 65bbb28..844b8b5 100644 --- a/aztec_code_generator.py +++ b/aztec_code_generator.py @@ -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. """ @@ -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}, @@ -160,14 +159,14 @@ 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) @@ -175,11 +174,13 @@ def reed_solomon(wd, nd, nc, gf, pp): 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} @@ -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 @@ -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', @@ -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 @@ -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] @@ -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' @@ -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 = '' @@ -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) @@ -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: @@ -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 = [] @@ -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]) @@ -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): @@ -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 @@ -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 @@ -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 @@ -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') @@ -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) @@ -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!') @@ -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() From f9109583a7f6695dcba2ca982d73ed8a08d33fb7 Mon Sep 17 00:00:00 2001 From: Zazzik1 Date: Mon, 16 Sep 2024 13:22:30 +0200 Subject: [PATCH 3/4] Added the SVGFactory class, added support for SVG files in the AztecCode.save method --- README.md | 12 ++++-- aztec_code_generator.py | 49 ++++++++++++++++++++++++ test_aztec_code_generator.py | 73 +++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2acb0bb..5f6d8bc 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,28 @@ PIL - Python Imaging Library (or Pillow) ## Usage: +This code will generate an image file "aztec_code.png" with the Aztec Code that contains "Aztec Code 2D :)" text: ```python data = 'Aztec Code 2D :)' aztec_code = AztecCode(data) aztec_code.save('aztec_code.png', module_size=4) ``` -This code will generate an image file "aztec_code.png" with the Aztec Code that contains "Aztec Code 2D :)" text. - ![Aztec Code](https://1.bp.blogspot.com/-OZIo4dGwAM4/V7BaYoBaH2I/AAAAAAAAAwc/WBdTV6osTb4TxNf2f6v7bCfXM4EuO4OdwCLcB/s1600/aztec_code.png "Aztec Code with data") +The generator supports SVG images, but without the upper text. In this case the `module_size` parameter has no effect and can be ommited, as the SVG images are scalable without loss of quality: +```python +aztec_code = AztecCode('the data to store') +aztec_code.save('aztec_code.svg') +``` + +This code will print out resulting 19x19 (compact) Aztec Code to the standard output: ```python data = 'Aztec Code 2D :)' aztec_code = AztecCode(data) aztec_code.print_out() ``` -This code will print out resulting 19x19 (compact) Aztec Code to the standard output. - ``` ## # ## #### # ## ##### ### diff --git a/aztec_code_generator.py b/aztec_code_generator.py index 844b8b5..9ba63f9 100644 --- a/aztec_code_generator.py +++ b/aztec_code_generator.py @@ -539,6 +539,50 @@ def find_suitable_matrix_size(data): 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 == '#'): + """ 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'' + return SvgFactory(data) + + def save(self, filename): + """ Save SVG to image file + + :param filename: output image filename + :return: None + """ + with open(filename, 'w') as file: + file.write(self.svg_str) + + class AztecCode(object): """ Aztec code generator. @@ -582,6 +626,10 @@ def save(self, filename, module_size=1): :return: None. """ + if filename.lower().endswith('.svg'): + image = SvgFactory.create_svg(self.matrix) + image.save(filename) + return if ImageDraw is None: exc = missing_pil[0](missing_pil[1]) exc.__traceback__ = missing_pil[2] @@ -849,6 +897,7 @@ def main(): print('PIL is not installed, cannot generate PNG') else: aztec_code.save('aztec_code.png', 4) + aztec_code.save('aztec_code.svg') print('Aztec Code info: {0}x{0} {1}'.format(aztec_code.size, '(compact)' if aztec_code.compact else '')) diff --git a/test_aztec_code_generator.py b/test_aztec_code_generator.py index 68e78cc..2c0be2f 100644 --- a/test_aztec_code_generator.py +++ b/test_aztec_code_generator.py @@ -2,8 +2,14 @@ #-*- coding: utf-8 -*- import unittest +from unittest.mock import patch, mock_open, MagicMock from aztec_code_generator import ( - reed_solomon, find_optimal_sequence, optimal_sequence_to_bits, get_data_codewords, + reed_solomon, + find_optimal_sequence, + optimal_sequence_to_bits, + get_data_codewords, + SvgFactory, + AztecCode, ) @@ -87,6 +93,71 @@ def test_get_data_codewords(self): self.assertEqual(get_data_codewords('111111', 6), [0b111110, 0b111110]) self.assertEqual(get_data_codewords('111101111101', 6), [0b111101, 0b111101]) +class TestSvgFactory(unittest.TestCase): + def test_init(self): + data = 'example svg data' + instance = SvgFactory(data) + self.assertEqual(instance.svg_str, data) + + @patch('builtins.open', new_callable=mock_open) + def test_save(self, mock): + data = '' + filename = 'example_filename.svg' + instance = SvgFactory(data) + mock.reset_mock() + instance.save(filename) + mock.assert_called_once_with(filename, "w") + mock().write.assert_called_once_with(data) + + def test_create_svg(self): + CASES = [ + dict( + matrix = [['#','#','#'], [' ',' ',' '], ['#','#','#']], + snapshot = '', + ), + dict( + matrix = [['#',' ',' '], [' ','#',' '], [' ',' ','#']], + snapshot = '', + ), + dict( + matrix = [['#',' '], [' ','#']], + snapshot = '' + ), + dict( + matrix = [['#',' '], [' ','#']], + border = 3, + snapshot = '' + ), + dict( + matrix = [[1,0], [0,1]], + matching_fn = lambda x: x == 1, + snapshot = '' + ), + ] + for case in CASES: + if "border" in case: + instance = SvgFactory.create_svg(case["matrix"], border=case["border"]) + elif "matching_fn" in case: + instance = SvgFactory.create_svg(case["matrix"], matching_fn=case["matching_fn"]) + else: + instance = SvgFactory.create_svg(case["matrix"]) + self.assertIsInstance(instance, SvgFactory) + self.assertEqual( + instance.svg_str, + case["snapshot"], + 'should match snapshot' + ) + +class TestAztecCode(unittest.TestCase): + def test_save_should_support_svg(self): + """ Should call SvgFactory.save if file extension ends with .svg """ + mock_svg_factory_save = MagicMock() + SvgFactory.save = mock_svg_factory_save + aztec_code = AztecCode('example data') + filename = 'file.svg' + aztec_code.save(filename) + mock_svg_factory_save.assert_called_once_with(filename) + if __name__ == '__main__': unittest.main(verbosity=2) From 07137251fbae6910ffcd37abe322c5503f5c90ad Mon Sep 17 00:00:00 2001 From: Zazzik1 Date: Sat, 28 Sep 2024 13:36:03 +0200 Subject: [PATCH 4/4] test: added negative test case (if not svg) for save method; updated docstring --- aztec_code_generator.py | 1 + test_aztec_code_generator.py | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/aztec_code_generator.py b/aztec_code_generator.py index fb0d697..52239cf 100755 --- a/aztec_code_generator.py +++ b/aztec_code_generator.py @@ -612,6 +612,7 @@ def __create_matrix(self): 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 \ diff --git a/test_aztec_code_generator.py b/test_aztec_code_generator.py index ac581a1..a4f838d 100644 --- a/test_aztec_code_generator.py +++ b/test_aztec_code_generator.py @@ -265,7 +265,29 @@ def test_create_svg(self): ) class TestAztecCode(unittest.TestCase): - def test_save_should_support_svg(self): + def test_save_should_use_PIL_if_not_SVG(self): + aztec_code = AztecCode('example data') + class Image: + def save(): + pass + image_mock = Image() + image_mock.save = MagicMock() + with patch.object(aztec_code, 'image', return_value=image_mock): + # filename .png, format None + aztec_code.save('image.png') + image_mock.save.assert_called_once() + image_mock.save.reset_mock() + + # filename .jpg, format None + aztec_code.save('image.jpg') + image_mock.save.assert_called_once() + image_mock.save.reset_mock() + + # filename .svg, format 'PNG' + aztec_code.save('image.svg', format='PNG') + image_mock.save.assert_called_once() + + def test_save_should_support_SVG(self): """ Should call SvgFactory.save for SVG files """ mock_svg_factory_save = MagicMock() SvgFactory.save = mock_svg_factory_save @@ -277,9 +299,9 @@ def test_save_should_support_svg(self): mock_svg_factory_save.assert_called_once_with(filename) mock_svg_factory_save.reset_mock() - # filename != .svg, format 'svg' + # filename != .svg, format 'SVG' filename = 'file.png' - aztec_code.save(filename, format='svg') + aztec_code.save(filename, format='SVG') mock_svg_factory_save.assert_called_once_with(filename) mock_svg_factory_save.reset_mock()