Skip to content

Commit

Permalink
Updated Django QR Code to use Segno instead of qrcode / Pillow. #13
Browse files Browse the repository at this point in the history
Removes the requirement "Pillow" since Segno creates PNG images without further dependencies
Support for other colors than black and white, each module type may have its own color.
Added support for Micro QR codes.
Removed the image.py module, not needed anymore.
  • Loading branch information
philippe-docourt committed Sep 7, 2020
1 parent 8ba89c2 commit c2ebbc8
Show file tree
Hide file tree
Showing 64 changed files with 916 additions and 852 deletions.
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
indent_style = space
indent_size = 4
insert_final_newline = true
charset = utf-8

[*.html]
indent_size = 2
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## 2.0.0 (2020-09-??)
* Removed dependency on Pillow / qrcode
* Switched to [Segno](https://pypi.org/project/segno/) for generating QR Codes
* Added support for QR Codes with multiple colors
* Added support for Micro QR Codes

## 1.3.1 (2020-09-07)
* Fix local testing script.
* Fix date of release 1.3.0 in readme.
Expand Down Expand Up @@ -107,4 +113,4 @@ The changes mentioned above might break the compatibility with code using qr_cod
* Improved examples in demo app.

## 0.1.1 (2017-08-02)
First public release.
First public release.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

This is an application that provides tools for displaying QR codes on your [Django](https://www.djangoproject.com/) site.

This application depends on the [qrcode](https://github.com/lincolnloop/python-qrcode) python library which requires the [Pillow](https://github.com/python-pillow/Pillow) library in order to support the PNG image format.
The Pillow library needs to be installed manually if you want to generate QR codes in PNG format; otherwise, SVG is the only supported format.
This application depends on the [Segno QR Code generator](https://pypi.org/project/segno/) library.

This app makes no usage of the Django models and therefore do not use any database.

Expand Down Expand Up @@ -66,7 +65,7 @@ The `size` parameter gives the size of each module of the QR code matrix. It can
* l or L: large (value: 30)
* h or H: huge (value: 48)

For PNG image format the size unit is in pixels, while the unit is 0.1 mm for SVG format.
For PNG image format the size unit is in pixels, while the unit is 1 mm for SVG format.

Here is a "hello world" QR code using the version 12:
```djangotemplate
Expand Down
7 changes: 0 additions & 7 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ API Reference
=============


qrcode.image
------------

.. automodule:: qr_code.qrcode.image
:members:


qrcode.maker
-------

Expand Down
14 changes: 5 additions & 9 deletions qr_code/qrcode/constants.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
from datetime import datetime

from qrcode import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H

from qr_code.qrcode.image import SVG_FORMAT_NAME

QR_CODE_GENERATION_VERSION_DATE = datetime(year=2019, month=4, day=11, hour=15)
QR_CODE_GENERATION_VERSION_DATE = datetime(year=2020, month=9, day=8, hour=12)
SIZE_DICT = {'t': 6, 's': 12, 'm': 18, 'l': 30, 'h': 48}
ERROR_CORRECTION_DICT = {'L': ERROR_CORRECT_L, 'M': ERROR_CORRECT_M, 'Q': ERROR_CORRECT_Q, 'H': ERROR_CORRECT_H}
DEFAULT_MODULE_SIZE = 'M'
ERROR_CORRECTION_DICT = {'L': 'l', 'M': 'm', 'Q': 'q', 'H': 'h'}
DEFAULT_MODULE_SIZE = 'm'
DEFAULT_BORDER_SIZE = 4
DEFAULT_VERSION = None
DEFAULT_IMAGE_FORMAT = SVG_FORMAT_NAME
DEFAULT_ERROR_CORRECTION = 'M'
DEFAULT_IMAGE_FORMAT = 'svg'
DEFAULT_ERROR_CORRECTION = 'm'
DEFAULT_CACHE_ENABLED = True
DEFAULT_URL_SIGNATURE_ENABLED = True

Expand Down
35 changes: 0 additions & 35 deletions qr_code/qrcode/image.py

This file was deleted.

133 changes: 46 additions & 87 deletions qr_code/qrcode/maker.py
Original file line number Diff line number Diff line change
@@ -1,116 +1,75 @@
"""Tools for generating QR codes. This module depends on the qrcode python library."""

import base64
from io import BytesIO

import xml.etree.ElementTree as ET

"""Tools for generating QR codes. This module depends on the Segno library."""
import io
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe

from qr_code.qrcode.constants import SIZE_DICT, ERROR_CORRECTION_DICT, DEFAULT_ERROR_CORRECTION, DEFAULT_MODULE_SIZE, \
DEFAULT_CACHE_ENABLED, DEFAULT_URL_SIGNATURE_ENABLED
from qr_code.qrcode.image import SvgPathImage, PilImageOrFallback, SVG_FORMAT_NAME, PNG_FORMAT_NAME
import segno
from qr_code.qrcode.constants import DEFAULT_CACHE_ENABLED, \
DEFAULT_URL_SIGNATURE_ENABLED
from qr_code.qrcode.serve import make_qr_code_url
from qr_code.qrcode.utils import QRCodeOptions


class SvgEmbeddedInHtmlImage(SvgPathImage):
def _write(self, stream):
self._img.append(self.make_path())
ET.ElementTree(self._img).write(stream, encoding="UTF-8", xml_declaration=False, default_namespace=None,
method='html')


def make_qr_code_image(text, image_factory, qr_code_options=QRCodeOptions()):
"""
Generates an image object (from the qrcode library) representing the QR code for the given text.
def make_qr(text, qr_code_options):
"""Creates a QR code
Any invalid argument is silently converted into the default value for that argument.
:rtype: segno.QRCode
"""

valid_version = _get_valid_version_or_none(qr_code_options.version)
valid_size = _get_valid_size_or_default(qr_code_options.size)
valid_error_correction = _get_valid_error_correction_or_default(qr_code_options.error_correction)
import qrcode
qr = qrcode.QRCode(
version=valid_version,
error_correction=valid_error_correction,
box_size=valid_size,
border=qr_code_options.border
)
qr.add_data(force_str(text))
if valid_version is None:
qr.make(fit=True)
return qr.make_image(image_factory=image_factory)


def _get_valid_error_correction_or_default(error_correction):
return ERROR_CORRECTION_DICT.get(error_correction.upper(), ERROR_CORRECTION_DICT[
DEFAULT_ERROR_CORRECTION])


def _get_valid_size_or_default(size):
if _can_be_cast_to_int(size):
actual_size = int(size)
if actual_size < 1:
actual_size = SIZE_DICT[DEFAULT_MODULE_SIZE.lower()]
elif isinstance(size, str):
actual_size = SIZE_DICT.get(size.lower(), DEFAULT_MODULE_SIZE)
else:
actual_size = SIZE_DICT[DEFAULT_MODULE_SIZE.lower()]
return actual_size
return segno.make(force_str(text), **qr_code_options.kw_make())


def _get_valid_version_or_none(version):
if _can_be_cast_to_int(version):
actual_version = int(version)
if actual_version < 1 or actual_version > 40:
actual_version = None
else:
actual_version = None
return actual_version

def make_qr_code_image(text, qr_code_options):
"""
Returns a bytes object representing a QR code image for the provided text.
def _can_be_cast_to_int(value):
return isinstance(value, int) or (isinstance(value, str) and value.isdigit())
:param str text: The text to encode
:param qr_code_options: Options to create and serialize the QR code.
:rtype: bytes
"""
qr = make_qr(text, qr_code_options)
out = io.BytesIO()
qr.save(out, **qr_code_options.kw_save())
return out.getvalue()


def make_embedded_qr_code(text, qr_code_options=QRCodeOptions()):
def make_embedded_qr_code(text, qr_code_options):
"""
Generates a <svg> or <img> tag representing the QR code for the given text. This tag can be embedded into an
HTML document.
Generates a <svg> or <img> tag representing the QR code for the given text.
This tag can be embedded into an HTML document.
"""
image_format = qr_code_options.image_format
img = make_qr_code_image(text, SvgEmbeddedInHtmlImage if image_format == SVG_FORMAT_NAME else PilImageOrFallback, qr_code_options=qr_code_options)
stream = BytesIO()
if image_format == SVG_FORMAT_NAME:
img.save(stream, kind=SVG_FORMAT_NAME.upper())
html_fragment = (str(stream.getvalue(), 'utf-8'))
else:
img.save(stream, format=PNG_FORMAT_NAME.upper())
html_fragment = '<img src="data:image/png;base64, %s" alt="%s">' % (str(base64.b64encode(stream.getvalue()), encoding='ascii'), escape(text))
return mark_safe(html_fragment)
qr = make_qr(text, qr_code_options)
kw = qr_code_options.kw_save()
# Pop the image format from the keywords since qr.png_data_uri / qr.svg_inline
# set it automatically
kw.pop('kind')
if qr_code_options.image_format == 'png':
return mark_safe('<img src="{0}" alt="{1}">'
.format(qr.png_data_uri(**kw), escape(text)))
return mark_safe(qr.svg_inline(**kw))


def make_qr_code_with_args(text, qr_code_args):
options = qr_code_args.get('options')
if options:
if not isinstance(options, QRCodeOptions):
raise TypeError('The options argument must be of type QRCodeOptions.')
else:
options = QRCodeOptions(**qr_code_args)
options = _options_from_args(qr_code_args)
return make_embedded_qr_code(text, options)


def make_qr_code_url_with_args(text, qr_code_args):
cache_enabled = qr_code_args.pop('cache_enabled', DEFAULT_CACHE_ENABLED)
url_signature_enabled = qr_code_args.pop('url_signature_enabled', DEFAULT_URL_SIGNATURE_ENABLED)
options = qr_code_args.get('options')
options = _options_from_args(qr_code_args)
return make_qr_code_url(text, options, cache_enabled=cache_enabled,
url_signature_enabled=url_signature_enabled)


def _options_from_args(args):
"""Returns a QRCodeOptions instance from the provided arguments.
"""
options = args.get('options')
if options:
if not isinstance(options, QRCodeOptions):
raise TypeError('The options argument must be of type QRCodeOptions.')
else:
options = QRCodeOptions(**qr_code_args)
return make_qr_code_url(text, options, cache_enabled=cache_enabled, url_signature_enabled=url_signature_enabled)
# Convert the string "None" into None
kw = {k: v if v != 'None' else None for k, v in args.items()}
options = QRCodeOptions(**kw)
return options
38 changes: 25 additions & 13 deletions qr_code/qrcode/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,45 @@ def qr_code_last_modified(request):
return constants.QR_CODE_GENERATION_VERSION_DATE


def make_qr_code_url(text, qr_code_options=QRCodeOptions(), cache_enabled=None, url_signature_enabled=None):
"""
Build an URL to a view that handle serving QR code image from the given parameters. Any invalid argument related
to the size or the format of the image is silently converted into the default value for that argument.
def make_qr_code_url(text, qr_code_options=None, cache_enabled=None,
url_signature_enabled=None):
"""Build an URL to a view that handle serving QR code image from the given parameters.
The parameter *cache_enabled (bool)* allows to skip caching the QR code (when set to *False*) when caching has
been enabled.
Any invalid argument related to the size or the format of the image is silently
converted into the default value for that argument.
The parameter *url_signature_enabled (bool)* tells whether the random token for protecting the URL against
external requests is added to the returned URL. It defaults to *True*.
:param bool cache_enabled: Allows to skip caching the QR code (when set to *False*) when caching has
been enabled.
:param bool url_signature_enabled: Tells whether the random token for protecting the URL against
external requests is added to the returned URL. It defaults to *True*.
"""
qr_code_options = QRCodeOptions() if qr_code_options is None else qr_code_options
if url_signature_enabled is None:
url_signature_enabled = constants.DEFAULT_URL_SIGNATURE_ENABLED
if cache_enabled is None:
cache_enabled = constants.DEFAULT_CACHE_ENABLED
encoded_text = str(base64.urlsafe_b64encode(bytes(force_str(text), encoding='utf-8')), encoding='utf-8')

image_format = qr_code_options.image_format
params = dict(text=encoded_text, size=qr_code_options.size, border=qr_code_options.border, version=qr_code_options.version or '', image_format=image_format, error_correction=qr_code_options.error_correction, cache_enabled=cache_enabled)
params = dict(text=encoded_text, cache_enabled=cache_enabled)
# Only add non-default values to the params dict
if qr_code_options.size != constants.DEFAULT_MODULE_SIZE:
params['size'] = qr_code_options.size
if qr_code_options.border != constants.DEFAULT_BORDER_SIZE:
params['border'] = qr_code_options.border
if qr_code_options.version != constants.DEFAULT_VERSION:
params['version'] = qr_code_options.version
if qr_code_options.image_format != constants.DEFAULT_IMAGE_FORMAT:
params['image_format'] = qr_code_options.image_format
if qr_code_options.error_correction != constants.DEFAULT_ERROR_CORRECTION:
params['error_correction'] = qr_code_options.error_correction
if qr_code_options.micro:
params['micro'] = qr_code_options.micro
params.update(qr_code_options.color_mapping())
path = reverse('qr_code:serve_qr_code_image')

if url_signature_enabled:
# Generate token to handle view protection. The token is added to the query arguments. It does not replace
# existing plain text query arguments in order to allow usage of the URL as an API (without token since external
# users cannot generate the signed token!).
token = get_qr_url_protection_signed_token(qr_code_options)
params['token'] = token

url = '%s?%s' % (path, urllib.parse.urlencode(params))
return mark_safe(url)
Loading

0 comments on commit c2ebbc8

Please sign in to comment.