diff --git a/docs/source/conf.py b/docs/source/conf.py index dc60d6627..5592bcaab 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -246,6 +246,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'pvcheetah', 'pyotp', 'linode_api4', + 'pyzbar', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/docs/source/platypush/plugins/qrcode.rst b/docs/source/platypush/plugins/qrcode.rst new file mode 100644 index 000000000..9b6b1dea5 --- /dev/null +++ b/docs/source/platypush/plugins/qrcode.rst @@ -0,0 +1,5 @@ +``platypush.plugins.qrcode`` +============================ + +.. automodule:: platypush.plugins.qrcode + :members: diff --git a/docs/source/platypush/responses/qrcode.rst b/docs/source/platypush/responses/qrcode.rst new file mode 100644 index 000000000..3b10ad068 --- /dev/null +++ b/docs/source/platypush/responses/qrcode.rst @@ -0,0 +1,5 @@ +``platypush.message.response.qrcode`` +===================================== + +.. automodule:: platypush.message.response.qrcode + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index c0cbf0356..445c58c32 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -87,6 +87,7 @@ Plugins platypush/plugins/ping.rst platypush/plugins/printer.cups.rst platypush/plugins/pushbullet.rst + platypush/plugins/qrcode.rst platypush/plugins/redis.rst platypush/plugins/sensor.rst platypush/plugins/serial.rst diff --git a/docs/source/responses.rst b/docs/source/responses.rst index 8e8ab6ddb..26b532ab4 100644 --- a/docs/source/responses.rst +++ b/docs/source/responses.rst @@ -15,6 +15,7 @@ Responses platypush/responses/pihole.rst platypush/responses/ping.rst platypush/responses/printer.cups.rst + platypush/responses/qrcode.rst platypush/responses/stt.rst platypush/responses/system.rst platypush/responses/todoist.rst diff --git a/platypush/backend/http/app/routes/plugins/qrcode/__init__.py b/platypush/backend/http/app/routes/plugins/qrcode/__init__.py new file mode 100644 index 000000000..dfb684074 --- /dev/null +++ b/platypush/backend/http/app/routes/plugins/qrcode/__init__.py @@ -0,0 +1,33 @@ +import base64 + +from flask import abort, request, Blueprint, Response + +from platypush.backend.http.app import template_folder +from platypush.context import get_plugin +from platypush.plugins.qrcode import QrcodePlugin + +qrcode = Blueprint('qrcode', __name__, template_folder=template_folder) + +# Declare routes list +__routes__ = [ + qrcode, +] + + +@qrcode.route('/qrcode', methods=['GET']) +def generate_code(): + """ + This route can be used to generate a QR code given a ``content`` parameter. + """ + + content = request.args.get('content') + if not content: + abort(400, 'Expected content parmeter') + + plugin: QrcodePlugin = get_plugin('qrcode') + response = plugin.generate(content, format='png').output + data = base64.decodebytes(response['data'].encode()) + return Response(data, mimetype='image/png') + + +# vim:sw=4:ts=4:et: diff --git a/platypush/message/response/qrcode.py b/platypush/message/response/qrcode.py new file mode 100644 index 000000000..d16ec1a40 --- /dev/null +++ b/platypush/message/response/qrcode.py @@ -0,0 +1,62 @@ +import base64 +from typing import Optional, List + +from pyzbar.pyzbar import Decoded +from pyzbar.locations import Rect + +from platypush.message import Mapping +from platypush.message.response import Response + + +class QrcodeResponse(Response): + pass + + +class QrcodeGeneratedResponse(QrcodeResponse): + # noinspection PyShadowingBuiltins + def __init__(self, + content: str, + format: str, + data: Optional[str] = None, + image_file: Optional[str] = None, + *args, **kwargs): + super().__init__(*args, output={ + 'text': content, + 'data': data, + 'format': format, + 'image_file': image_file, + }, **kwargs) + + +class RectModel(Mapping): + def __init__(self, rect: Rect): + super().__init__() + self.left = rect.left + self.top = rect.top + self.width = rect.width + self.height = rect.height + + +class ResultModel(Mapping): + def __init__(self, result: Decoded, *args, **kwargs): + super().__init__(*args, **kwargs) + # noinspection PyBroadException + try: + data = result.data.decode() + except: + data = base64.encodebytes(result.data).decode() + + self.data = data + self.type = result.type + self.rect = dict(RectModel(result.rect)) if result.rect else {} + + +class QrcodeDecodedResponse(QrcodeResponse): + def __init__(self, results: List[Decoded], image_file: Optional[str] = None, *args, **kwargs): + super().__init__(*args, output={ + 'image_file': image_file, + 'results': [dict(ResultModel(result)) for result in results], + }, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/qrcode.py b/platypush/plugins/qrcode.py new file mode 100644 index 000000000..c5d6a5edc --- /dev/null +++ b/platypush/plugins/qrcode.py @@ -0,0 +1,80 @@ +import base64 +import io +import os +from typing import Optional + +from platypush.message.response.qrcode import QrcodeGeneratedResponse, QrcodeDecodedResponse +from platypush.plugins import Plugin, action + + +class QrcodePlugin(Plugin): + """ + Plugin to generate and scan QR and bar codes. + + Requires: + + * **qrcode** (``pip install 'qrcode[pil]'``) for QR generation. + * **pyzbar** (``pip install pyzbar``) for decoding code from images. + * **Pillow** (``pip install Pillow``) for image management. + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # noinspection PyShadowingBuiltins + @action + def generate(self, content: str, output_file: Optional[str] = None, show: bool = False, + format: str = 'png') -> QrcodeGeneratedResponse: + """ + Generate a QR code. + If you configured the :class`:platypush.backend.http.HttpBackend` then you can also generate + codes directly from the browser through ``http://:/qrcode?content=...``. + + :param content: Text, URL or content of the QR code. + :param output_file: If set then the QR code will be exported in the specified image file. + Otherwise, a base64-encoded representation of its binary content will be returned in + the response as ``data``. + :param show: If True, and if the device where the application runs has an active display, + then the generated QR code will be shown on display. + :param format: Output image format (default: ``png``). + :return: :class:`platypush.message.response.qrcode.QrcodeGeneratedResponse`. + """ + import qrcode + qr = qrcode.make(content) + img = qr.get_image() + ret = { + 'content': content, + 'format': format, + } + + if show: + img.show() + if output_file: + output_file = os.path.abspath(os.path.expanduser(output_file)) + img.save(output_file, format=format) + ret['image_file'] = output_file + else: + f = io.BytesIO() + img.save(f, format=format) + ret['data'] = base64.encodebytes(f.getvalue()).decode() + + return QrcodeGeneratedResponse(**ret) + + @action + def decode(self, image_file: str) -> QrcodeDecodedResponse: + """ + Decode a QR code from an image file. + + :param image_file: Path of the image file. + """ + from pyzbar import pyzbar + from PIL import Image + + image_file = os.path.abspath(os.path.expanduser(image_file)) + img = Image.open(image_file) + results = pyzbar.decode(img) + return QrcodeDecodedResponse(results) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index aad0efb5b..a7678af58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -246,3 +246,8 @@ croniter # Support for Linode integration # linode_api4 + +# Support for QR codes +# qrcode +# Pillow +# pyzbar diff --git a/setup.py b/setup.py index 2a9cc7102..f2f11a3e1 100755 --- a/setup.py +++ b/setup.py @@ -293,5 +293,7 @@ setup( 'otp': ['pyotp'], # Support for Linode integration 'linode': ['linode_api4'], + # Support for QR codes + 'qrcode': ['qrcode[pil]', 'Pillow', 'pyzbar'], }, )