From 727da71cf3471a99ee1a86e83bd693777ef27820 Mon Sep 17 00:00:00 2001 From: Johanna England Date: Tue, 21 Mar 2023 11:05:53 +0100 Subject: [PATCH] Add util function to generate qr code for url --- changelog.d/2887.added.md | 1 + python/nav/web/utils.py | 57 +++++++++++++++++++- requirements/base.txt | 2 + tests/unittests/web/generate_qr_code_test.py | 11 ++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2887.added.md create mode 100644 tests/unittests/web/generate_qr_code_test.py diff --git a/changelog.d/2887.added.md b/changelog.d/2887.added.md new file mode 100644 index 0000000000..28c12cd66b --- /dev/null +++ b/changelog.d/2887.added.md @@ -0,0 +1 @@ +Add functionality to create QR codes to a given url \ No newline at end of file diff --git a/python/nav/web/utils.py b/python/nav/web/utils.py index 62f179526a..e6bae9d110 100644 --- a/python/nav/web/utils.py +++ b/python/nav/web/utils.py @@ -14,12 +14,17 @@ # along with NAV. If not, see . # """Utils for views""" +import base64 +import io +import os from django.http import HttpResponse - - from django.views.generic.list import ListView +import qrcode +from PIL import ImageDraw, ImageFont +import qrcode.image.pil + def get_navpath_root(): """Returns the default navpath root @@ -51,6 +56,7 @@ def require_param(parameter): Will check both GET and POST querydict for the parameter. """ + # pylint: disable=missing-docstring def wrap(func): def wrapper(request, *args, **kwargs): @@ -64,3 +70,50 @@ def wrapper(request, *args, **kwargs): return wrapper return wrap + + +def generate_qr_code(url: str, name: str = "") -> qrcode.image.pil.PilImage: + """ + Generate a QR code from a given url, and, if given, adds the name below as a + caption + + Returns the generated image as a PilImage object + """ + # Creating QR code + qr = qrcode.QRCode(box_size=10) + qr.add_data(url) + img = qr.make_image() + draw = ImageDraw.Draw(img) + + # Adding the name as caption + if name: + img_width, img_height = img.size + font_dir = os.path.dirname(__file__) + if len(name) < 25: + font = ImageFont.truetype( + os.path.join(font_dir, "static/fonts/OS600.woff"), 25 + ) + elif len(name) < 50: + font = ImageFont.truetype( + os.path.join(font_dir, "static/fonts/OS600.woff"), 15 + ) + else: + font = ImageFont.truetype( + os.path.join(font_dir, "static/fonts/OS600.woff"), 10 + ) + caption_width = font.getlength(name) + draw.text( + ((img_width - caption_width) / 2, img_height - 40), + text=name, + font=font, + fill="black", + ) + + return img + + +def convert_pil_object_to_bytes_string(pil_image: qrcode.image.pil.PilImage) -> str: + file_object = io.BytesIO() + pil_image.save(file_object, "PNG") + pil_image.close() + return base64.b64encode(file_object.getvalue()).decode('utf-8') diff --git a/requirements/base.txt b/requirements/base.txt index 4af7fd589e..fbb564dc24 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -9,7 +9,9 @@ pyaml twisted~=23.8.0 # last version that still supports Python 3.7 networkx==2.6.3 +# Cannot be removed as long as qrcode is included Pillow>3.3.2 +qrcode>7.4 pyrad==2.1 sphinx==5.3.0 sphinxcontrib-programoutput==0.17 diff --git a/tests/unittests/web/generate_qr_code_test.py b/tests/unittests/web/generate_qr_code_test.py new file mode 100644 index 0000000000..92c27d468b --- /dev/null +++ b/tests/unittests/web/generate_qr_code_test.py @@ -0,0 +1,11 @@ +import qrcode.image.pil +from nav.web.utils import convert_pil_object_to_bytes_string, generate_qr_code + +import qrcode + + +def test_generate_qr_code_returns_string(): + qr_code = generate_qr_code(url="www.example.com", name="buick.lab.uninett.no") + assert isinstance(qr_code, qrcode.image.pil.PilImage) + qr_code_bytes_string = convert_pil_object_to_bytes_string(qr_code) + assert isinstance(qr_code_bytes_string, str)