diff --git a/.github/workflows/test_and_release.yml b/.github/workflows/test_and_release.yml
index 8d96478..c3a6b3c 100644
--- a/.github/workflows/test_and_release.yml
+++ b/.github/workflows/test_and_release.yml
@@ -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
@@ -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
@@ -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:
diff --git a/README.md b/README.md
index 622c8be..566941a 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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).
diff --git a/aztec_code_generator.py b/aztec_code_generator.py
index 1b5f22b..52239cf 100755
--- a/aztec_code_generator.py
+++ b/aztec_code_generator.py
@@ -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.
"""
@@ -17,6 +17,8 @@
import codecs
from collections import namedtuple
from enum import Enum
+from io import IOBase
+
try:
from PIL import Image, ImageDraw
@@ -163,14 +165,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)
@@ -178,11 +180,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}
@@ -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
@@ -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
@@ -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 = ''
@@ -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)]
@@ -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''
+ 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):
@@ -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
@@ -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):
@@ -608,7 +670,7 @@ 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):
@@ -616,7 +678,7 @@ def __add_finder_pattern(self):
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
@@ -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
@@ -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
@@ -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
@@ -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()
diff --git a/requirements-test.txt b/requirements-test.txt
index 47fb2b9..68185e3 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -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
diff --git a/setup.py b/setup.py
index b2de56c..ae80931 100755
--- a/setup.py
+++ b/setup.py
@@ -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',
diff --git a/test_aztec_code_generator.py b/test_aztec_code_generator.py
index 9b23fa5..a4f838d 100644
--- a/test_aztec_code_generator.py
+++ b/test_aztec_code_generator.py
@@ -2,9 +2,18 @@
#-*- 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, encoding_to_eci,
- Mode, Latch, Shift, Misc,
+ Mode,
+ Latch,
+ Shift,
+ Misc,
+ reed_solomon,
+ find_optimal_sequence,
+ optimal_sequence_to_bits,
+ get_data_codewords,
+ encoding_to_eci,
+ SvgFactory,
AztecCode,
)
@@ -190,6 +199,117 @@ def test_barcode_readability_eci(self):
self._encode_and_decode(r, 'The price is €4', encoding='utf-8')
self._encode_and_decode(r, 'אין לי מושג', encoding='iso8859-8')
+class TestSvgFactory(unittest.TestCase):
+ def test_init(self):
+ 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_save_with_provided_file_handler(self):
+ data = ''
+ with NamedTemporaryFile(mode='w+') as fp:
+ instance = SvgFactory(data)
+ instance.save(fp)
+ fp.flush()
+ fp.seek(0)
+ saved_content = fp.read()
+ self.assertEqual(saved_content, data)
+
+ def test_create_svg(self):
+ CASES = [
+ dict(
+ matrix = [[1, 1, 1], [0 ,0 ,0 ], [1, 1, 1]],
+ snapshot = '',
+ ),
+ dict(
+ matrix = [[1, 0, 0], [0, 1, 0], [0, 0 ,1]],
+ snapshot = '',
+ ),
+ dict(
+ matrix = [[1, 0], [0, 1]],
+ snapshot = ''
+ ),
+ dict(
+ matrix = [[1, 0], [0, 1]],
+ border = 3,
+ snapshot = ''
+ ),
+ dict(
+ matrix = [['#', ' '], [' ', '#']],
+ matching_fn = lambda x: x == '#',
+ 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_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
+ aztec_code = AztecCode('example data')
+
+ # filename .svg, format None
+ filename = 'file.svg'
+ aztec_code.save(filename)
+ mock_svg_factory_save.assert_called_once_with(filename)
+ mock_svg_factory_save.reset_mock()
+
+ # filename != .svg, format 'SVG'
+ filename = 'file.png'
+ aztec_code.save(filename, format='SVG')
+ mock_svg_factory_save.assert_called_once_with(filename)
+ mock_svg_factory_save.reset_mock()
+
+ # filename is a file object, format 'svg'
+ with NamedTemporaryFile() as fp:
+ aztec_code.save(fp, format='svg')
+ mock_svg_factory_save.assert_called_once_with(fp)
+
if __name__ == '__main__':
unittest.main(verbosity=2)