From c81545546170480113b2a3fdc49fc3bf06b373b8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 8 Jan 2025 12:06:00 +1100 Subject: [PATCH] Datamatrix (#8853) * Implement datamatrix barcode generation * Update documentation * Update package requirements * Add unit test * Raise error on empty barcode data * Update docs/hooks.py --- docs/docs/assets/images/report/datamatrix.png | Bin 0 -> 5832 bytes docs/docs/assets/images/report/qrcode.png | Bin 0 -> 6418 bytes docs/docs/hooks.py | 9 +- docs/docs/report/barcodes.md | 124 +++++++++++++++--- src/backend/InvenTree/report/helpers.py | 2 +- .../InvenTree/report/templatetags/barcode.py | 116 ++++++++++++++-- src/backend/InvenTree/report/tests.py | 29 ++++ src/backend/requirements.in | 1 + src/backend/requirements.txt | 4 + 9 files changed, 251 insertions(+), 34 deletions(-) create mode 100644 docs/docs/assets/images/report/datamatrix.png create mode 100644 docs/docs/assets/images/report/qrcode.png diff --git a/docs/docs/assets/images/report/datamatrix.png b/docs/docs/assets/images/report/datamatrix.png new file mode 100644 index 0000000000000000000000000000000000000000..0d575476cef70376d3de5b980a834cd9bd6f835b GIT binary patch literal 5832 zcmeHLdrVtZ7{A2Fuw)}K$P%IO;y{qMDUi;VQAR5|HrG1XXqyg5M`FeO<1YXgstFOleMcW((Ct~)2{yEKPE#3`^Qat zPjAn?=R3dGcR#MGDA~@*;4locy{xoQ$S_ICaBz|zhY?@ywE%o1DTO5kjCt;9FPuDj zIA4&@Fc(j!{9gYUoIi1_v_{D=JFeo9G}|Djmr0% zR8mng{3SiFtT6xW6Vf}5mxgzJYkuQKFZ)}@(y{RD#rLv%Qn?*zH=ld+o8m6DFOQ9^ z&9F|%Tao*Izel3@syozd8jqZ5$#O|nzuVB0OW|AR*i1f8of19b-*DuGvn^S?y`xo^ zg_hV6{{|zP`R>rA`Kz471Y4&DvVMzbrNU9ysn42U~AncSC{x{vs6QRUL$bS z>8uWw>r`N3Ec>oh9KJGkW76m~m%EQxSBqK~U7#;K?M>J1I)|#-;M?O4K0AF!F*o(O zF4w0Bn2%UTgc`lCwQK1f|M1ea^2`vw)wRbh8p;u9rc0aF^oWboYZ=oDy>EEotZ`D> zZ_(OO4raBVF7X8#0wRErZTWgzjrV6;v0rbT;MOu4?aEnq0h+E(ag@8aZ80wPS2P^y0!OYz0*R6JiWhXk-TezQLNI z+QDpr<|*8ukERLTZRMT;!Wt+!7o}xK8_@=>&Q{rX$k5og42ps}^vpdXBw?=hT!9_; z5VnnwZ0jxnRt_ZtH5Eaf#Vh6(tXtl%iQ#{~CV*PvR_K@2aZJ7fU`7UzVRT4Q9N*xs zgiT}^pO{=pxe}X%E`&s>2fT|;lDOT&DCSP@VwybC(b!|0hOcp*eeYDfE#bR;uIO-E-1!=Vnt!8;HaM;}M)hI~m)LyUYRIXCKU228Hh z$k0MaG&NDX;;JERQg7PAOQW_6QJk7ex;&b`r-&UV#{aE4q{(fiO+1KBe}%@xPk-j- zf1(;7Dj+~yH4pn$z8|CUwqEFQ&qGiU=e!bqHX0=i~U zsG(Z)f763l7X$HuAzF+tV{@|pC8#GzSh*X!bcH7+q0ji0P!Wwc3jN@fPwF=`+RlgGijsD_U_{a#i< z8y3lGD8P=Bp5Oyhur16=NZ&Bw4DVfU-%Wap+KrrEVG7>5-u8u4(hm39sFTS3kcVBj fI3*T#J=AXZzPoE=Zkr5VG&5yI6@})4cRKz6IE4<; literal 0 HcmV?d00001 diff --git a/docs/docs/assets/images/report/qrcode.png b/docs/docs/assets/images/report/qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..da2b93fc5ee8239cc07c78460521d45dd5e10f6e GIT binary patch literal 6418 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 tag. +### Barcode Template Tags -Inside the template file (whether it be for printing a label or generating a custom report), the following code will need to be included at the top of the template file: +To use the barcode tags inside a label or report template, you must load the `barcode` template tags at the top of the template file: ```html {% raw %} @@ -18,12 +17,30 @@ Inside the template file (whether it be for printing a label or generating a cus {% endraw %} ``` -### 1D Barcode +### Barcode Image Data + +The barcode template tags will generate an image tag with the barcode data encoded as a base64 image. The image data is intended to be rendered as an `img` tag: + +```html +{% raw %} +{% load barcode %} + +{% endraw %} +``` + +## 1D Barcode !!! info "python-barcode" One dimensional barcodes (e.g. Code128) are generated using the [python-barcode](https://pypi.org/project/python-barcode/) library. -To render a 1D barcode, use the `barcode` template tag, as shown in the example below: +To render a 1D barcode, use the `barcode` template tag: + +::: report.templatetags.barcode.barcode + options: + show_docstring_description: False + show_source: False + +### Example ```html {% raw %} @@ -36,6 +53,8 @@ To render a 1D barcode, use the `barcode` template tag, as shown in the example {% endraw %} ``` +### Additional Options + The default barcode renderer will generate a barcode using [Code128](https://en.wikipedia.org/wiki/Code_128) rendering. However [other barcode formats](https://python-barcode.readthedocs.io/en/stable/supported-formats.html) are also supported: ```html @@ -58,29 +77,102 @@ You can also pass further [python-barcode](https://python-barcode.readthedocs.io {% endraw %} ``` -### QR-Code +## QR-Code !!! info "qrcode" Two dimensional QR codes are generated using the [qrcode](https://pypi.org/project/qrcode/) library. To render a QR code, use the `qrcode` template tag: -```html -{% raw %} +::: report.templatetags.barcode.qrcode + options: + show_docstring_description: false + show_source: False -{% load barcode %} - - -{% endraw %} -``` - -Additional parameters can be passed to the `qrcode` function for rendering: +### Example ```html {% raw %} - +{% extends "label/label_base.html" %} + +{% load l10n i18n barcode %} + +{% block style %} + +.qr { + position: absolute; + left: 0mm; + top: 0mm; + {% localize off %} + height: {{ height }}mm; + width: {{ height }}mm; + {% endlocalize %} +} + +{% endblock style %} + +{% block content %} + +{% endblock content %} {% endraw %} ``` +which produces the following output: + +{% with id="qrcode", url="report/qrcode.png", description="QR Code" %} +{% include 'img.html' %} +{% endwith %} + + !!! tip "Documentation" Refer to the [qrcode library documentation](https://pypi.org/project/qrcode/) for more information + + +## Data Matrix + +!!! info "ppf.datamatrix" + Data Matrix codes are generated using the [ppf.datamatrix](https://pypi.org/project/ppf-datamatrix/) library. + +[Data Matrix Codes](https://en.wikipedia.org/wiki/Data_Matrix) provide an alternative to QR codes for encoding data in a two-dimensional matrix. To render a Data Matrix code, use the `datamatrix` template tag: + +::: report.templatetags.barcode.datamatrix + options: + show_docstring_description: false + show_source: False + +### Example + +```html +{% raw %} +{% extends "label/label_base.html" %} + +{% load l10n i18n barcode %} + +{% block style %} + +.qr { + position: absolute; + left: 0mm; + top: 0mm; + {% localize off %} + height: {{ height }}mm; + width: {{ height }}mm; + {% endlocalize %} +} + +{% endblock style %} + +{% block content %} + + + + +{% endblock content %} +{% endraw %} +``` + +which produces the following output: + +{% with id="datamatrix", url="report/datamatrix.png", description="Datamatrix barcode" %} +{% include 'img.html' %} +{% endwith %} diff --git a/src/backend/InvenTree/report/helpers.py b/src/backend/InvenTree/report/helpers.py index 04e328da8a..b6899fce8d 100644 --- a/src/backend/InvenTree/report/helpers.py +++ b/src/backend/InvenTree/report/helpers.py @@ -78,7 +78,7 @@ def report_page_size_default(): return page_size -def encode_image_base64(image, img_format: str = 'PNG'): +def encode_image_base64(image, img_format: str = 'PNG') -> str: """Return a base-64 encoded image which can be rendered in an tag. Arguments: diff --git a/src/backend/InvenTree/report/templatetags/barcode.py b/src/backend/InvenTree/report/templatetags/barcode.py index 8b32576f5e..1aef3046a3 100644 --- a/src/backend/InvenTree/report/templatetags/barcode.py +++ b/src/backend/InvenTree/report/templatetags/barcode.py @@ -5,6 +5,7 @@ from django.utils.safestring import mark_safe import barcode as python_barcode import qrcode.constants as ECL +from PIL import Image, ImageColor from qrcode.main import QRCode import report.helpers @@ -19,7 +20,7 @@ QR_ECL_LEVEL_MAP = { } -def image_data(img, fmt='PNG'): +def image_data(img, fmt='PNG') -> str: """Convert an image into HTML renderable data. Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an tag @@ -45,26 +46,31 @@ def clean_barcode(data): @register.simple_tag() -def qrcode(data, **kwargs): +def qrcode(data: str, **kwargs) -> str: """Return a byte-encoded QR code image. Arguments: data: Data to encode Keyword Arguments: - version: QR code version, (None to auto detect) (default = None) - error_correction: Error correction level (L: 7%, M: 15%, Q: 25%, H: 30%) (default = 'M') - box_size: pixel dimensions for one black square pixel in the QR code (default = 20) - border: count white QR square pixels around the qr code, needed as padding (default = 1) - optimize: data will be split into multiple chunks of at least this length using different modes (text, alphanumeric, binary) to optimize the QR code size. Set to `0` to disable. (default = 1) - format: Image format (default = 'PNG') - fill_color: Fill color (default = "black") - back_color: Background color (default = "white") + version (int): QR code version, (None to auto detect) (default = None) + error_correction (str): Error correction level (L: 7%, M: 15%, Q: 25%, H: 30%) (default = 'M') + box_size (int): pixel dimensions for one black square pixel in the QR code (default = 20) + border (int): count white QR square pixels around the qr code, needed as padding (default = 1) + optimize (int): data will be split into multiple chunks of at least this length using different modes (text, alphanumeric, binary) to optimize the QR code size. Set to `0` to disable. (default = 1) + format (str): Image format (default = 'PNG') + fill_color (str): Fill color (default = "black") + back_color (str): Background color (default = "white") Returns: - base64 encoded image data + image (str): base64 encoded image data """ + data = str(data).strip() + + if not data: + raise ValueError("No data provided to 'qrcode' template tag") + # Extract other arguments from kwargs fill_color = kwargs.pop('fill_color', 'black') back_color = kwargs.pop('back_color', 'white') @@ -89,8 +95,26 @@ def qrcode(data, **kwargs): @register.simple_tag() -def barcode(data, barcode_class='code128', **kwargs): - """Render a barcode.""" +def barcode(data: str, barcode_class='code128', **kwargs) -> str: + """Render a 1D barcode. + + Arguments: + data: Data to encode + + Keyword Arguments: + format (str): Image format (default = 'PNG') + fill_color (str): Foreground color (default = 'black') + back_color (str): Background color (default = 'white') + scale (float): Scaling factor (default = 1) + + Returns: + image (str): base64 encoded image data + """ + data = str(data).strip() + + if not data: + raise ValueError("No data provided to 'barcode' template tag") + constructor = python_barcode.get_barcode_class(barcode_class) img_format = kwargs.pop('format', 'PNG') @@ -105,3 +129,69 @@ def barcode(data, barcode_class='code128', **kwargs): # Render to byte-encoded image return image_data(image, fmt=img_format) + + +@register.simple_tag() +def datamatrix(data: str, **kwargs) -> str: + """Render a DataMatrix barcode. + + Arguments: + data: Data to encode + + Keyword Arguments: + fill_color (str): Foreground color (default = 'black') + back_color (str): Background color (default = 'white') + scale (float): Matrix scaling factor (default = 1) + border (int): Border width (default = 1) + + Returns: + image (str): base64 encoded image data + """ + from ppf.datamatrix import DataMatrix + + data = str(data).strip() + + if not data: + raise ValueError("No data provided to 'datamatrix' template tag") + + dm = DataMatrix(data) + + fill_color = kwargs.pop('fill_color', 'black') + back_color = kwargs.pop('back_color', 'white') + + border = kwargs.pop('border', 1) + + try: + border = int(border) + except Exception: + border = 1 + + border = max(0, border) + + try: + fg = ImageColor.getcolor(fill_color, 'RGB') + except Exception: + fg = ImageColor.getcolor('black', 'RGB') + + try: + bg = ImageColor.getcolor(back_color, 'RGB') + except Exception: + bg = ImageColor.getcolor('white', 'RGB') + + scale = kwargs.pop('scale', 1) + + height = len(dm.matrix) + 2 * border + width = len(dm.matrix[0]) + 2 * border + + # Generate raw image from the matrix + img = Image.new('RGB', (width, height), color=bg) + + for y, row in enumerate(dm.matrix): + for x, value in enumerate(row): + if value: + img.putpixel((x + border, y + border), fg) + + if scale != 1: + img = img.resize((int(width * scale), int(height * scale))) + + return image_data(img, fmt='PNG') diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index 8a8daf3ede..f410da2aec 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -240,6 +240,10 @@ class BarcodeTagTest(TestCase): self.assertIsInstance(barcode, str) self.assertTrue(barcode.startswith('data:image/bmp;')) + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.barcode('') + def test_qrcode(self): """Test the qrcode generation tag.""" # Test with default settings @@ -256,6 +260,31 @@ class BarcodeTagTest(TestCase): self.assertTrue(qrcode.startswith('data:image/bmp;')) self.assertEqual(len(qrcode), 309720) + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.qrcode('') + + def test_datamatrix(self): + """Test the datamatrix generation tag.""" + # Test with default settings + datamatrix = barcode_tags.datamatrix('hello world') + self.assertEqual( + datamatrix, + 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAAlElEQVR4nJ1TQQ7AIAgri///cncw6wroEseBgEFbCgZJnNsFICKOPAAIjeSM5T11IznK5f5WRMgnkhP9JfCcTC/MxFZ5hxLOgqrn3o/z/OqtsNpdSL31Iu9W4Dq8Sulu+q5Nuqa3XYOdnuidlICPpXhZVBruyzAKSZehT+yNlzvZQcq6JiW7Ni592swf/43kdlDfdgMk1eOtR7kWpAAAAABJRU5ErkJggg==', + ) + + datamatrix = barcode_tags.datamatrix( + 'hello world', border=3, fill_color='red', back_color='blue' + ) + self.assertEqual( + datamatrix, + 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAqElEQVR4nN1UQQ6AMAgrxv9/GQ9mpJYSY/QkBxM3KLUUA0i8i+1l/dcQiXj09CwSEU2aQJ7nE8ou2faVUXoPZSEkq+dZKVxWg4UqxUHnVdkp6IdwMXMulGvzNBDMk4WwPSrUF3LNnQNZBJmOsZaVXa44QSEKnvWb5mIgKon1E1H6aPyOcIa15uhONP9aR4hSCiGmYAoYpj4uO+vK4+ybMhr8Nkjmn/z4Dvoldi8uJu4iAAAAAElFTkSuQmCC', + ) + + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.datamatrix('') + class ReportTest(InvenTreeAPITestCase): """Base class for unit testing reporting models.""" diff --git a/src/backend/requirements.in b/src/backend/requirements.in index 9558aaaa74..024621c4ef 100644 --- a/src/backend/requirements.in +++ b/src/backend/requirements.in @@ -40,6 +40,7 @@ pdf2image # PDF to image conversion pillow # Image manipulation pint # Unit conversion pip-licenses # License information for installed packages +ppf.datamatrix # Data Matrix barcode generator python-barcode[images] # Barcode generator python-dotenv # Environment variable management pyyaml>=6.0.1 # YAML parsing diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index cf75977595..c9ca9627bd 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -1192,6 +1192,10 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via pint +ppf-datamatrix==0.2 \ + --hash=sha256:819be65eae444b760e178d5761853f78f8e5fca14fec2809b5e3369978fa9244 \ + --hash=sha256:8f034d9c90e408f60f8b10a273baab81014c9a81c983dc1ebdc31d4ca5ac5582 + # via -r src/backend/requirements.in prettytable==3.12.0 \ --hash=sha256:77ca0ad1c435b6e363d7e8623d7cc4fcf2cf15513bf77a1c1b2e814930ac57cc \ --hash=sha256:f04b3e1ba35747ac86e96ec33e3bb9748ce08e254dc2a1c6253945901beec804