Skip to content

Commit 528e38b

Browse files
committed
Document template support
Signed-off-by: DL6ER <[email protected]>
1 parent baadbaa commit 528e38b

8 files changed

Lines changed: 98 additions & 44 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
}
2727
},
2828
"runArgs": [
29-
// "-v",
30-
// "/dev/bus/lp0:/dev/bus/lp0",
31-
// "/dev/bus/lp1:/dev/bus/lp1",
32-
// "/dev/bus/lp2:/dev/bus/lp2"
29+
"-e",
30+
"TZ=Europe/Berlin"
31+
// "-v",
32+
// "/dev/usb/lp0:/dev/usb/lp0",
33+
// "/dev/usb/lp1:/dev/usb/lp1",
34+
// "/dev/usb/lp2:/dev/usb/lp2"
3335
]
3436
}

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ Additional printer support comes from [matmair/brother_ql-inventree](https://git
3434

3535
![Native dark mode](./screenshots/image6.png)
3636

37+
### Template support
38+
39+
![Template support](./screenshots/image7.png)
40+
3741
## New Features
3842

3943
- Automatic printer and label detection
@@ -57,6 +61,8 @@ Additional printer support comes from [matmair/brother_ql-inventree](https://git
5761
- **QL-1110NWB**
5862
- **QL-1115NWB**
5963
- Support individual fonts/sizes and spacing for each line of text on the labels
64+
- Dynamic content replacement, e.g., using `{{datetime}}` and `{{counter}}` templates
65+
- Import and export of labels in an easily editable format (JSON)
6066
- Allow text inversion for emphasized text even without color
6167
- Auto-fit images best onto the labels to avoid cropping
6268
- Allow text together with images

app/labeldesigner/label.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from enum import Enum, auto
2+
import os
3+
import uuid
24
from qrcode import QRCode, constants
35
from PIL import Image, ImageDraw, ImageFont
46
import logging
@@ -94,7 +96,8 @@ def __init__(
9496
self.barcode_type = barcode_type
9597
self._label_margin = label_margin
9698
self._fore_color = fore_color
97-
self.text = text
99+
self.text = None
100+
self.input_text = text
98101
self._qr_size = qr_size
99102
self.qr_correction = qr_correction
100103
self._image = image
@@ -162,7 +165,47 @@ def label_type(self):
162165
def label_type(self, value):
163166
self._label_type = value
164167

168+
def process_templates(self):
169+
# Loop over text lines and replace
170+
# {{datetime:x}} by current datetime in specified format x
171+
# {{counter}} by an incrementing counter
172+
self.text = self.input_text.copy()
173+
for line in self.text:
174+
# Replace {{counter}} with current counter value
175+
line['text'] = line['text'].replace("{{counter}}", str(self._counter))
176+
177+
# Replace {{datetime:x}} with current datetime formatted as x
178+
def datetime_replacer(match):
179+
fmt = match.group(1)
180+
if self._timestamp > 0:
181+
now = datetime.datetime.fromtimestamp(self._timestamp)
182+
else:
183+
now = datetime.datetime.now()
184+
return now.strftime(fmt)
185+
line['text'] = re.sub(r"\{\{datetime:([^}]+)\}\}", datetime_replacer, line['text'])
186+
187+
# Replace {{uuid}} with a new UUID
188+
if "{{uuid}}" in line['text']:
189+
line['text'] = line['text'].replace("{{uuid}}", str(uuid.uuid4()))
190+
191+
# Replace {{short-uuid}} with a shortened UUID
192+
if "{{short-uuid}}" in line['text']:
193+
line['text'] = line['text'].replace("{{short-uuid}}", str(uuid.uuid4())[:8])
194+
195+
# Replace {{env:var}} with the value of the environment variable var
196+
def env_replacer(match):
197+
var_name = match.group(1)
198+
return os.getenv(var_name, "")
199+
line['text'] = re.sub(r"\{\{env:([^}]+)\}\}", env_replacer, line['text'])
200+
201+
# Increment counter
202+
self._counter += 1
203+
165204
def generate(self, rotate = False):
205+
# Process possible templates in the text
206+
self.process_templates()
207+
208+
# Generate codes or load images if requested
166209
if self._label_content in (LabelContent.QRCODE_ONLY, LabelContent.TEXT_QRCODE):
167210
if self.barcode_type == "QR":
168211
img = self._generate_qr()
@@ -348,22 +391,6 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
348391
# Fix for completely empty text
349392
if len(self.text) == 0 or len(self.text[0]['text']) == 0:
350393
self.text[0]['text'] = " "
351-
352-
# Loop over text lines and replace
353-
# {{datetime:x}} by current datetime in specified format x
354-
# {{counter}} by an incrementing counter
355-
for line in self.text:
356-
line['text'] = line['text'].replace("{{counter}}", str(self._counter))
357-
# Replace {{datetime:x}} with current datetime formatted as x
358-
def datetime_replacer(match):
359-
fmt = match.group(1)
360-
if self._timestamp > 0:
361-
now = datetime.datetime.fromtimestamp(self._timestamp)
362-
else:
363-
now = datetime.datetime.now()
364-
return now.strftime(fmt)
365-
line['text'] = re.sub(r"\{\{datetime:([^}]+)\}\}", datetime_replacer, line['text'])
366-
self._counter += 1
367394

368395
# Iterate over lines of text
369396
for i, line in enumerate(self.text):

app/labeldesigner/templates/labeldesigner.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,33 @@
324324
<fieldset class="form-group" id="groupLabelText">
325325
<label for="labelText" style="margin-top: 10px; margin-bottom: 0">Label Text:</label>
326326
<textarea rows="7" id="labelText" class="form-control" onChange="preview()" onInput="preview()" data-default=""></textarea>
327+
<!-- Template info box -->
328+
<div class="card mt-2" style="font-size: 0.95em;">
329+
<div class="card-header p-2" style="cursor: pointer; user-select: none;" data-toggle="collapse"
330+
data-target="#templateInfoBox" aria-expanded="false" aria-controls="templateInfoBox">
331+
<span class="fas fa-info-circle" aria-hidden="true"></span>
332+
<span style="margin-left: 0.5em;">About label templates <span
333+
class="fas fa-chevron-down float-right"></span></span>
334+
</div>
335+
<div id="templateInfoBox" class="collapse">
336+
<div class="card-body pt-2 pb-2">
337+
<strong>Templates</strong> allow you to insert dynamic content into your label text. Supported
338+
templates:<br>
339+
<ul style="margin-bottom: 0.5em;">
340+
<li><code>{{'{{datetime:%H:%M:%S %d.%m.%Y'}}}}</code> — Inserts the current date and time when printing.
341+
You can customize the format using <a href="https://strftime.org/" target="_blank"
342+
rel="noopener">strftime</a> options.</li>
343+
<li><code>{{'{{counter'}}}}</code> — Inserts an up-counting number, increasing for every printed label.
344+
</li>
345+
<li><code>{{'{{uuid'}}}}</code> — Inserts a random UUID (Universally Unique Identifier).</li>
346+
<li><code>{{'{{short-uuid'}}}}</code> — Inserts a shortened version of a UUID.</li>
347+
<li><code>{{'{{env:var}}'}}</code> — Inserts the value of the environment variable <code>var</code>.
348+
</li>
349+
</ul>
350+
<span class="text-muted">Templates are replaced at print time.</span>
351+
</div>
352+
</div>
353+
</div>
327354
</fieldset>
328355
</div>
329356
<div class="col-md-4">

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ services:
66
restart: always
77
ports:
88
- "8013:8013"
9+
environment:
10+
- TZ=Europe/Berlin
911
# devices:
1012
# - "/dev/usb/lp0:/dev/usb/lp0"
13+
volumes:
14+
- /etc/timezone:/etc/timezone:ro
1115
command: >
1216
--model QL-800
1317
--default-label-size 62

screenshots/image7.png

238 KB
Loading

tests/template.png

9.29 KB
Loading

tests/test_labeldesigner_api.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def verify_image(response_data, expected_image_path):
2424
if not UPDATE_IMAGES and os.path.isfile(expected_image_path):
2525
with open(expected_image_path, 'rb') as f:
2626
expected_data = f.read()
27-
assert response_data == expected_data
28-
return
27+
if response_data != expected_data:
28+
raise AssertionError("Generated image does not match expected image")
2929

3030
# Write image into file
3131
with open(expected_image_path, 'wb') as f:
@@ -333,38 +333,26 @@ def test_image_black_fit(client):
333333
image_test(client, image_mode="black", fit=True)
334334

335335

336-
def test_generate_datetime(client):
336+
def test_generate_template(client):
337337
data = EXAMPLE_FORMDATA.copy()
338338
# Mock current datetime.now
339-
data['timestamp'] = int(datetime(2023, 1, 1, 12, 0, 0).timestamp())
339+
data['timestamp'] = int(datetime(2023, 3, 18, 12, 15, 30).timestamp())
340340

341341
data['text'] = json.dumps([
342342
{
343343
'font_family': 'DejaVu Sans',
344344
'font_style': 'Regular',
345-
'text': '{{datetime:%d.%m.%Y %H:%M:%S}}',
345+
'text': '{{datetime:%d.%m.%Y %H:%M:%S}} COUNTER: {{counter}}',
346346
'font_size': '30',
347347
'align': 'left'
348-
}
349-
])
350-
351-
response = client.post('/labeldesigner/api/preview', data=data)
352-
assert response.status_code == 200
353-
assert response.content_type in ['image/png', 'text/plain']
354-
355-
# Check image
356-
verify_image(response.data, 'tests/datetime.png')
357-
358-
359-
def test_generate_counter(client):
360-
data = EXAMPLE_FORMDATA.copy()
361-
data['text'] = json.dumps([
348+
},
362349
{
363350
'font_family': 'DejaVu Sans',
364351
'font_style': 'Regular',
365-
'text': '{{counter}}',
366-
'font_size': '60',
367-
'align': 'left'
352+
'text': '>> {{datetime:Label created at %H:%M on %m/%d/%y}} <<',
353+
'font_size': '20',
354+
'align': 'right',
355+
'font_inverted': True
368356
}
369357
])
370358

@@ -373,7 +361,7 @@ def test_generate_counter(client):
373361
assert response.content_type in ['image/png', 'text/plain']
374362

375363
# Check image
376-
verify_image(response.data, 'tests/counter.png')
364+
verify_image(response.data, 'tests/template.png')
377365

378366

379367
# We cannot test the print functionality without a physical printer

0 commit comments

Comments
 (0)