diff --git a/changelog.d/2887.added.md b/changelog.d/2887.added.md new file mode 100644 index 0000000000..afb294747b --- /dev/null +++ b/changelog.d/2887.added.md @@ -0,0 +1 @@ +Add library utilities to produce QR codes to arbitrary URLs, for use in upcoming features \ No newline at end of file diff --git a/python/nav/web/utils.py b/python/nav/web/utils.py index 62f179526a..e8003b4ef7 100644 --- a/python/nav/web/utils.py +++ b/python/nav/web/utils.py @@ -14,12 +14,18 @@ # along with NAV. If not, see . # """Utils for views""" +import base64 +import io +import os +from typing import Dict, List 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 +57,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 +71,58 @@ def wrapper(request, *args, **kwargs): return wrapper return wrap + + +def generate_qr_code(url: str, caption: str = "") -> io.BytesIO: + """ + Generate a QR code from a given url, and, if given, adds a caption to it + + Returns the generated image as a bytes buffer + """ + # Creating QR code + qr = qrcode.QRCode(box_size=10) + qr.add_data(url) + img = qr.make_image() + draw = ImageDraw.Draw(img) + + # Adding caption + if caption: + img_width, img_height = img.size + font_path = os.path.join(os.path.dirname(__file__), "static/fonts/OS600.woff") + if len(caption) < 25: + font = ImageFont.truetype(font_path, 25) + elif len(caption) < 50: + font = ImageFont.truetype(font_path, 15) + else: + font = ImageFont.truetype(font_path, 10) + caption_width = font.getlength(caption) + draw.text( + ((img_width - caption_width) / 2, img_height - 40), + text=caption, + font=font, + fill="black", + ) + + file_object = io.BytesIO() + img.save(file_object, "PNG") + img.close() + + return file_object + + +def convert_bytes_buffer_to_bytes_string(bytes_buffer: io.BytesIO) -> str: + return base64.b64encode(bytes_buffer.getvalue()).decode('utf-8') + + +def generate_qr_codes_as_byte_strings(url_dict: Dict[str, str]) -> List[str]: + """ + Takes a dict of the form {name:url} and returns a list of generated QR codes as + byte strings + """ + qr_code_byte_strings = [] + for caption, url in url_dict.items(): + qr_code_byte_buffer = generate_qr_code(url=url, caption=caption) + qr_code_byte_strings.append( + convert_bytes_buffer_to_bytes_string(bytes_buffer=qr_code_byte_buffer) + ) + return qr_code_byte_strings diff --git a/requirements/base.txt b/requirements/base.txt index e32753d118..c48565959d 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/qrcode_test.py b/tests/unittests/web/qrcode_test.py new file mode 100644 index 0000000000..0237c1875b --- /dev/null +++ b/tests/unittests/web/qrcode_test.py @@ -0,0 +1,16 @@ +import io + +from nav.web.utils import generate_qr_code, generate_qr_codes_as_byte_strings + + +def test_generate_qr_code_returns_byte_buffer(): + qr_code = generate_qr_code(url="www.example.com", caption="buick.lab.uninett.no") + assert isinstance(qr_code, io.BytesIO) + + +def test_generate_qr_codes_as_byte_strings_returns_list_of_byte_strings(): + qr_codes = generate_qr_codes_as_byte_strings( + {"buick.lab.uninett.no": "www.example.com"} + ) + assert isinstance(qr_codes, list) + assert isinstance(qr_codes[0], str)