11from enum import Enum , auto
2+ import os
3+ import uuid
24from qrcode import QRCode , constants
35from PIL import Image , ImageDraw , ImageFont
46import logging
57import barcode
68from barcode .writer import ImageWriter
9+ import datetime
10+ import re
711
812logger = 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" :
0 commit comments