Skip to content

Commit 847457c

Browse files
authored
Merge pull request pklaus#7 from DL6ER/enhance
Add template and import-export features
2 parents 5d6235c + 0849fb2 commit 847457c

11 files changed

Lines changed: 374 additions & 129 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 7 additions & 5 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
]
34-
}
36+
}

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: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
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
57
import barcode
68
from barcode.writer import ImageWriter
9+
import datetime
10+
import re
711

812
logger = logging.getLogger(__name__)
913

@@ -82,7 +86,8 @@ def __init__(
8286
border_thickness=1,
8387
border_roundness=0,
8488
border_distance=(0, 0),
85-
border_color=(0, 0, 0)):
89+
border_color=(0, 0, 0),
90+
timestamp=0):
8691
self._width = width
8792
self._height = height
8893
self.label_content = label_content
@@ -91,7 +96,8 @@ def __init__(
9196
self.barcode_type = barcode_type
9297
self._label_margin = label_margin
9398
self._fore_color = fore_color
94-
self.text = text
99+
self.text = None
100+
self.input_text = text
95101
self._qr_size = qr_size
96102
self.qr_correction = qr_correction
97103
self._image = image
@@ -100,6 +106,8 @@ def __init__(
100106
self._border_roundness = border_roundness
101107
self._border_distance = border_distance
102108
self._border_color = border_color
109+
self._counter = 1
110+
self._timestamp = timestamp
103111

104112
@property
105113
def label_content(self):
@@ -108,10 +116,11 @@ def label_content(self):
108116
@label_content.setter
109117
def label_content(self, value):
110118
self._label_content = value
111-
112-
@property
113-
def want_text(self):
114-
return self._label_content not in (LabelContent.QRCODE_ONLY,) and len(self.text) > 0
119+
120+
def want_text(self, img):
121+
# We always want to draw text (even when empty) when no image is
122+
# provided to avoid an error 500 because we created no image at all
123+
return img is None or self._label_content not in (LabelContent.QRCODE_ONLY,) and len(self.text) > 0 and len(self.text[0]['text']) > 0
115124

116125
@property
117126
def need_image_text_distance(self):
@@ -156,7 +165,51 @@ def label_type(self):
156165
def label_type(self, value):
157166
self._label_type = value
158167

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+
# Performance issue mitigation
186+
if len(line['text']) < 100:
187+
line['text'] = re.sub(r"\{\{datetime:([^}]+)\}\}", datetime_replacer, line['text'])
188+
189+
# Replace {{uuid}} with a new UUID
190+
if "{{uuid}}" in line['text']:
191+
line['text'] = line['text'].replace("{{uuid}}", str(uuid.uuid4()))
192+
193+
# Replace {{short-uuid}} with a shortened UUID
194+
if "{{short-uuid}}" in line['text']:
195+
line['text'] = line['text'].replace("{{short-uuid}}", str(uuid.uuid4())[:8])
196+
197+
# Replace {{env:var}} with the value of the environment variable var
198+
def env_replacer(match):
199+
var_name = match.group(1)
200+
return os.getenv(var_name, "")
201+
# Performance issue mitigation
202+
if len(line['text']) < 100:
203+
line['text'] = re.sub(r"\{\{env:([^}]+)\}\}", env_replacer, line['text'])
204+
205+
# Increment counter
206+
self._counter += 1
207+
159208
def generate(self, rotate = False):
209+
# Process possible templates in the text
210+
self.process_templates()
211+
212+
# Generate codes or load images if requested
160213
if self._label_content in (LabelContent.QRCODE_ONLY, LabelContent.TEXT_QRCODE):
161214
if self.barcode_type == "QR":
162215
img = self._generate_qr()
@@ -221,7 +274,7 @@ def generate(self, rotate = False):
221274
else:
222275
img_width, img_height = (0, 0)
223276

224-
if self.want_text:
277+
if self.want_text(img):
225278
bboxes = self._draw_text(None, [])
226279
textsize = self._compute_bbox(bboxes)
227280
else:
@@ -276,7 +329,7 @@ def generate(self, rotate = False):
276329
if img is not None:
277330
imgResult.paste(img, image_offset)
278331

279-
if self.want_text:
332+
if self.want_text(img):
280333
self._draw_text(imgResult, bboxes, text_offset)
281334

282335
# Check if the image needs rotation (only applied when generating
@@ -338,7 +391,12 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
338391
img = Image.new('L', (20, 20), 'white')
339392
draw = ImageDraw.Draw(img)
340393
y = 0
341-
logger.warning("Drawing text with offset %s", text_offset)
394+
395+
# Fix for completely empty text
396+
if len(self.text) == 0 or len(self.text[0]['text']) == 0:
397+
self.text[0]['text'] = " "
398+
399+
# Iterate over lines of text
342400
for i, line in enumerate(self.text):
343401
color = self._fore_color
344402

@@ -366,7 +424,7 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
366424
logger.error(f"Unsupported alignment: {align}")
367425
return
368426

369-
if do_draw and 'font_inverted' in line and line['font_inverted'] == "true":
427+
if do_draw and 'font_inverted' in line and line['font_inverted']:
370428
# Draw a filled rectangle
371429
center_x = 0
372430
if anchor == "lt":

app/labeldesigner/printer.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,21 @@ def process_queue(self) -> bool:
9292
logger.info('Label printed successfully and printer is ready for next job')
9393
return True
9494

95+
logger.warning("Failed to print label")
9596
return False
9697

9798
def get_ptr_status(device_specifier):
9899
status = {
99100
"errors": [],
100101
"path": device_specifier,
101-
"media_category": "DK",
102+
"media_category": None,
102103
"media_length": 0,
103-
"media_type": "Continuous length tape",
104-
"media_width": 62,
104+
"media_type": None,
105+
"media_width": None,
105106
"model": "Unknown",
106-
"model_code": 56,
107+
"model_code": None,
107108
"phase_type": "Unknown",
108-
"series_code": 52,
109+
"series_code": None,
109110
"setting": None,
110111
"status_code": 0,
111112
"status_type": "Unknown",

app/labeldesigner/routes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def get_printer_status():
102102

103103

104104
@bp.route('/api/print', methods=['POST', 'GET'])
105-
def print_text():
105+
def print_label():
106106
"""
107107
API to print a label
108108
@@ -188,6 +188,7 @@ def create_label_from_request(request):
188188
'image_bw_threshold': int(d.get('image_bw_threshold', 70)),
189189
'image_fit': int(d.get('image_fit', 1)) > 0,
190190
'print_color': d.get('print_color', 'black'),
191+
'timestamp': int(d.get('timestamp', 0))
191192
}
192193

193194
def get_label_dimensions(label_size):
@@ -299,5 +300,6 @@ def get_uploaded_image(image):
299300
border_thickness=context['border_thickness'],
300301
border_roundness=context['border_roundness'],
301302
border_distance=(context['border_distanceX'], context['border_distanceY']),
302-
border_color=border_color
303+
border_color=border_color,
304+
timestamp=context['timestamp']
303305
)

0 commit comments

Comments
 (0)