From b2b1ee72e8f3c89b122557d12fc6213a771c0d61 Mon Sep 17 00:00:00 2001 From: SebCorbin Date: Sat, 27 Jan 2024 22:53:25 +0100 Subject: [PATCH 1/4] Add test, cleanup some code --- jsignature/utils.py | 17 +++++++++-------- tests/test_fields.py | 23 ++++++++++++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/jsignature/utils.py b/jsignature/utils.py index c59cf72..e975cdd 100644 --- a/jsignature/utils.py +++ b/jsignature/utils.py @@ -21,19 +21,20 @@ def _remove_empty_pts(pt): 'y': list(filter(lambda n: n is not None, pt['y'])) } - if type(data) is str: + if isinstance(data, str): drawing = json.loads(data, object_hook=_remove_empty_pts) - elif type(data) is list: + elif isinstance(data, list): drawing = data else: raise ValueError # Compute box - min_width = int(round(min(chain(*[d['x'] for d in drawing])))) - 10 - max_width = int(round(max(chain(*[d['x'] for d in drawing])))) + 10 + padding = 10 + min_width = int(round(min(chain(*[d['x'] for d in drawing])))) - padding + max_width = int(round(max(chain(*[d['x'] for d in drawing])))) + padding width = max_width - min_width - min_height = int(round(min(chain(*[d['y'] for d in drawing])))) - 10 - max_height = int(round(max(chain(*[d['y'] for d in drawing])))) + 10 + min_height = int(round(min(chain(*[d['y'] for d in drawing])))) - padding + max_height = int(round(max(chain(*[d['y'] for d in drawing])))) + padding height = max_height - min_height # Draw image @@ -55,10 +56,10 @@ def _remove_empty_pts(pt): if bbox: im.crop(bbox) - old_pil_version = int(PIL_VERSION.split('.')[0]) < 10 im.thumbnail( (width, height), - Image.ANTIALIAS if old_pil_version else Image.LANCZOS + # Image.ANTIALIAS is replaced in PIL 10.0.0 + Image.ANTIALIAS if int(PIL_VERSION.split('.')[0]) < 10 else Image.LANCZOS ) if as_file: diff --git a/tests/test_fields.py b/tests/test_fields.py index e36f9de..ccc0341 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,17 +1,12 @@ import json +from django import forms from django.test import SimpleTestCase from django.core.exceptions import ValidationError from jsignature.fields import JSignatureField from jsignature.forms import JSignatureField as JSignatureFormField - -try: - from django.utils import six - - string_types = six.string_types -except ImportError: - string_types = str +from tests.models import JSignatureTestModel class JSignatureFieldTest(SimpleTestCase): @@ -46,7 +41,7 @@ def test_get_prep_value_correct_values_python(self): f = JSignatureField() val = [{"x": [1, 2], "y": [3, 4]}] val_prep = f.get_prep_value(val) - self.assertIsInstance(val_prep, string_types) + self.assertIsInstance(val_prep, str) self.assertEqual(val, json.loads(val_prep)) def test_get_prep_value_correct_values_json(self): @@ -54,7 +49,7 @@ def test_get_prep_value_correct_values_json(self): val = [{"x": [1, 2], "y": [3, 4]}] val_str = '[{"x":[1,2], "y":[3,4]}]' val_prep = f.get_prep_value(val_str) - self.assertIsInstance(val_prep, string_types) + self.assertIsInstance(val_prep, str) self.assertEqual(val, json.loads(val_prep)) def test_get_prep_value_incorrect_values(self): @@ -66,3 +61,13 @@ def test_formfield(self): f = JSignatureField() cls = f.formfield().__class__ self.assertTrue(issubclass(cls, JSignatureFormField)) + + def test_modelform_media(self): + class TestModelForm(forms.ModelForm): + class Meta: + model = JSignatureTestModel + fields = forms.ALL_FIELDS + + form = TestModelForm() + self.assertIn('jSignature.min.js', str(form.media)) + self.assertIn('django_jsignature.js', str(form.media)) From e8354d5d7aa93303c0595c116a935138e0f3775f Mon Sep 17 00:00:00 2001 From: SebCorbin Date: Sat, 27 Jan 2024 22:54:19 +0100 Subject: [PATCH 2/4] Drop pin on pillow --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cdec7d1..7edcb5c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' + open(os.path.join(here, 'CHANGES')).read(), license='LPGL, see LICENSE file.', - install_requires=['Django>=4.2', 'pillow<9.1.0', 'pyquery>=1.4.2'], + install_requires=['Django>=4.2', 'pillow', 'pyquery>=1.4.2'], packages=find_packages(exclude=['example_project*', 'tests']), include_package_data=True, zip_safe=False, From 868fb53ade75e71c14bb3f6b963211930fcc9ecf Mon Sep 17 00:00:00 2001 From: SebCorbin Date: Sat, 27 Jan 2024 23:27:40 +0100 Subject: [PATCH 3/4] Draw single points --- jsignature/utils.py | 55 +++++++++++++++++++++++++++----------------- tests/test_filter.py | 27 ++++++++++++++++++---- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/jsignature/utils.py b/jsignature/utils.py index e975cdd..56912a7 100644 --- a/jsignature/utils.py +++ b/jsignature/utils.py @@ -6,19 +6,19 @@ from itertools import chain from PIL import Image, ImageDraw, ImageOps, __version__ as PIL_VERSION -AA = 5 # super sampling gor antialiasing +AA = 5 # super sampling for antialiasing def draw_signature(data, as_file=False): - """ Draw signature based on lines stored in json_string. - `data` can be a json object (list in fact) or a json string - if `as_file` is True, a temp file is returned instead of Image instance + """Draw signature based on lines stored in json_string. + `data` can be a json object (list in fact) or a json string + if `as_file` is True, a temp file is returned instead of Image instance """ def _remove_empty_pts(pt): return { - 'x': list(filter(lambda n: n is not None, pt['x'])), - 'y': list(filter(lambda n: n is not None, pt['y'])) + "x": list(filter(lambda n: n is not None, pt["x"])), + "y": list(filter(lambda n: n is not None, pt["y"])), } if isinstance(data, str): @@ -30,26 +30,39 @@ def _remove_empty_pts(pt): # Compute box padding = 10 - min_width = int(round(min(chain(*[d['x'] for d in drawing])))) - padding - max_width = int(round(max(chain(*[d['x'] for d in drawing])))) + padding + min_width = int(round(min(chain(*[d["x"] for d in drawing])))) - padding + max_width = int(round(max(chain(*[d["x"] for d in drawing])))) + padding width = max_width - min_width - min_height = int(round(min(chain(*[d['y'] for d in drawing])))) - padding - max_height = int(round(max(chain(*[d['y'] for d in drawing])))) + padding + min_height = int(round(min(chain(*[d["y"] for d in drawing])))) - padding + max_height = int(round(max(chain(*[d["y"] for d in drawing])))) + padding height = max_height - min_height # Draw image im = Image.new("RGBA", (width * AA, height * AA)) draw = ImageDraw.Draw(im) - for line in drawing: - len_line = len(line['x']) - points = [ - ( - (line['x'][i] - min_width) * AA, - (line['y'][i] - min_height) * AA + for coords in drawing: + line_length = len(coords["x"]) + if line_length == 1: + # This is a single point, convert to a circle of 2x2 AA pixels + draw.ellipse( + [ + ( + (coords["x"][0] - min_width) * AA, + (coords["y"][0] - min_height) * AA, + ), + ( + (coords["x"][0] - min_width + 2) * AA, + (coords["y"][0] - min_height + 2) * AA, + ), + ], + fill="#000", ) - for i in range(0, len_line) - ] - draw.line(points, fill="#000", width=2 * AA) + else: + points = [ + ((coords["x"][i] - min_width) * AA, (coords["y"][i] - min_height) * AA) + for i in range(0, line_length) + ] + draw.line(points, fill="#000", width=2 * AA) im = ImageOps.expand(im) # Smart crop bbox = im.getbbox() @@ -59,11 +72,11 @@ def _remove_empty_pts(pt): im.thumbnail( (width, height), # Image.ANTIALIAS is replaced in PIL 10.0.0 - Image.ANTIALIAS if int(PIL_VERSION.split('.')[0]) < 10 else Image.LANCZOS + Image.ANTIALIAS if int(PIL_VERSION.split(".")[0]) < 10 else Image.LANCZOS, ) if as_file: - ret = im._dump(format='PNG') + ret = im._dump(format="PNG") else: ret = im diff --git a/tests/test_filter.py b/tests/test_filter.py index ce9bca9..6e83205 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -4,16 +4,33 @@ from jsignature.templatetags.jsignature_filters import signature_base64 -DUMMY_VALUE = [{"x": [205, 210], "y": [59, 63]}, - {"x": [205, 207], "y": [67, 64]}] +DUMMY_VALUE = [{"x": [205, 210], "y": [59, 63]}, {"x": [205, 207], "y": [67, 64]}] DUMMY_STR_VALUE = json.dumps(DUMMY_VALUE) class TemplateFilterTest(SimpleTestCase): def test_inputs_bad_type_value(self): - self.assertEqual(signature_base64(object()), '') - self.assertEqual(signature_base64(None), '') + self.assertEqual(signature_base64(object()), "") + self.assertEqual(signature_base64(None), "") def test_outputs_as_base64(self): output = signature_base64(DUMMY_STR_VALUE) - self.assertEqual(output, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAcCAYAAACUJBTQAAAAuElEQVR4nO3TMQ4BQRSH8d8uR9AQN5Do9Bo3wB3cQCs6lULcwZkkGmeQSBQU+yZEVIxC7FfNvGTnm7fvP9TU/A0NFN8UPB5eflMwQjvWzZyCMiQ9XLFHP7eoCFEL2xCdMMkleMUMl5AtVN1km1GhShcMcQjRKmqNF9+8JSnd59HFDoPYf9xNuuUUZ8w/PfCZlK4OjqpfNI5aU6bHmWK6DsEm9llmkCjcO1mqopxqv0mK8O92UFPzPjdRMBNmBFDqcwAAAABJRU5ErkJggg==") + self.assertEqual( + output, + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAcCAYAAACUJBTQAAAAuElEQVR4nO3TMQ4BQRSH8d8uR9AQN5Do9Bo3wB3cQCs6lULcwZkkGmeQSBQU+yZEVIxC7FfNvGTnm7fvP9TU/A0NFN8UPB5eflMwQjvWzZyCMiQ9XLFHP7eoCFEL2xCdMMkleMUMl5AtVN1km1GhShcMcQjRKmqNF9+8JSnd59HFDoPYf9xNuuUUZ8w/PfCZlK4OjqpfNI5aU6bHmWK6DsEm9llmkCjcO1mqopxqv0mK8O92UFPzPjdRMBNmBFDqcwAAAABJRU5ErkJggg==", + ) + + def test_outputs_as_base64_with_singlepoints(self): + face = [ + {"x": [117], "y": [88]}, + {"x": [140], "y": [83]}, + { + "x": [116, 121, 125, 129, 134, 139, 142, 145], + "y": [100, 100, 101, 102, 102, 102, 100, 97], + }, + ] + output = signature_base64(json.dumps(face)) + self.assertEqual( + output, + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAnCAYAAACmE6CaAAABmklEQVR4nO3XvUpcQRjG8d9xhYghhYFoIZgmbSCVQlIERAKpAl6GuYrchNeQFLmCRLGytdDCIpUEAmkNxIAfKWYGDrK6c9adXTeZP7zs7izn/ZiZ5505VCqVSuU/oIn2TzCVhbSTfjCxLO5Igzns4BuW49hMroPZMnllk1ahh2dYwUNcKbitSogv+VsSCmmPFaVkkKIrkCglvk46GDbAncRXkhxhT0R8JZiY+Eoy1QXcGx1UxkQjNJKppa23qdy6KekFbMbvI20iTR/rCWfNzAiCpQLm8VU4h97HsbFurWGLSofuPL4IBZzinYyOOOjEbqLDR/gcP69a/51gD/s4xp8+zw+iwblwJ/uIDfyKBezGAi4z/NwaAB4Lyd9mJ/iELbzQ7aL4EgfRzw+sx/Gs953cJe/hectpem4Br/Aaa8IlMfE9WlrNm+I3WI2/t/EBP2PMi8z8RsYi3sQk9vDb4NVLdoi3LV+dWmsX8d0k1kaYseuz/QRPB/hMN+EjnAmzf9nH19i43nK7MnQbLf2amVvMRGe/UqlUyvAXHBNDpT7g8gsAAAAASUVORK5CYII=", + ) From 90ea4e32d9ffdc6a86126a19918610610d505edd Mon Sep 17 00:00:00 2001 From: SebCorbin Date: Sat, 27 Jan 2024 23:31:49 +0100 Subject: [PATCH 4/4] Update version number --- CHANGES | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ac19c17..40ce0c5 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,22 @@ CHANGELOG ========= +0.12 (2024-01-27) +================== + +** Bug fixed ** + +- Draw single points + +** New ** + +- Updated Github Actions. +- Dropped support for python < 3.8 +- Dropped support for Django < 4.2 +- Add support for pillow >= 10 +- Updated jSignature to latest version + + 0.11 (2022-01-17) ================== diff --git a/setup.py b/setup.py index 7edcb5c..92c478f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='django-jsignature', - version='0.11', + version='0.12', author='Florent Lebreton', author_email='florent.lebreton@makina-corpus.com', url='https://github.com/fle/django-jsignature',