Skip to content

Commit a9c79a0

Browse files
authored
Merge pull request pklaus#15 from DL6ER/new/todo
Add TODO lines printing option
2 parents b9aa5a5 + 1f433d7 commit a9c79a0

8 files changed

Lines changed: 134 additions & 66 deletions

File tree

README.md

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

3939
![Template support](./screenshots/image7.png)
4040

41+
### TODO list creation
42+
43+
![TODO list creation](./screenshots/image8.png)
44+
4145
## New Features
4246

4347
- Automatic printer and label detection
@@ -65,6 +69,7 @@ Additional printer support comes from [matmair/brother_ql-inventree](https://git
6569
- Import and export of labels in an easily editable format (JSON)
6670
- Allow text inversion for emphasized text even without color
6771
- Auto-fit images best onto the labels to avoid cropping
72+
- Support for automatic TODO list creation (tickable checkboxes)
6873
- Allow text together with images
6974
- Print text as QR Code or barcode
7075
- Add text to QR Code

app/labeldesigner/label.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
404404

405405
# Determine anchors
406406
anchor = None
407-
align = line['align']
407+
align = line.get('align', 'center')
408408

409409
# Left aligned text
410410
if align == "left":
@@ -424,6 +424,9 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
424424
# raise ValueError("Red font is not supported on this label")
425425
color = (255, 0, 0) if red_font else (0, 0, 0)
426426

427+
# Draw TODO box if needed
428+
todo = line.get('todo', False)
429+
427430
if do_draw and 'inverted' in line and line['inverted']:
428431
# Draw a filled rectangle
429432
center_x = 0
@@ -455,22 +458,28 @@ def _draw_text(self, img = None, bboxes = [], text_offset = (0, 0)):
455458
bboxes.append((bbox, y))
456459
y += bbox[3] - bbox[1] + (spacing if i < len(self.text)-1 else 0)
457460
else:
461+
bbox = bboxes[i][0]
462+
y = bboxes[i][1] + text_offset[1]
458463
# Left aligned text
459464
if align == "left":
460465
min_bbox_x = min(bbox[0][0] for bbox in bboxes) if len(bboxes) > 0 else 0
461466
x = min_bbox_x + text_offset[0]
462-
y = bboxes[i][1] + text_offset[1]
463467
# Center aligned text
464468
elif align == "center":
465469
min_bbox_x = min(bbox[0][0] for bbox in bboxes) if len(bboxes) > 0 else 0
466470
max_bbox_x = max(bbox[0][2] for bbox in bboxes) if len(bboxes) > 0 else 0
467471
x = (max_bbox_x - min_bbox_x) // 2 + min_bbox_x + text_offset[0]
468-
y = bboxes[i][1] + text_offset[1]
469472
# Right aligned text
470473
elif align == "right":
471474
max_bbox_x = max(bbox[0][2] for bbox in bboxes) if len(bboxes) > 0 else 0
472475
x = max_bbox_x + text_offset[0]
473-
y = bboxes[i][1] + text_offset[1]
476+
477+
# Draw TODO box if needed
478+
if todo:
479+
todo_box_dimensions = 8 * int(line['size']) // 10
480+
bbox = draw.textbbox((x - 1.2 * todo_box_dimensions, y), line['text'], font=font, align=align, anchor=anchor)
481+
box_dimensions = bbox[0], y, bbox[0] + todo_box_dimensions, y + todo_box_dimensions
482+
draw.rounded_rectangle(box_dimensions, radius=5, outline=color, width=max(1, todo_box_dimensions//10), fill=(255,255,255))
474483

475484
draw.text((x, y), line['text'], color, font=font, anchor=anchor, align=align, spacing=spacing)
476485

app/labeldesigner/routes.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,10 @@ def get_label_dimensions(label_size):
195195

196196
def get_font_path(line: dict):
197197
try:
198-
family_name = line.get('family')
199-
style_name = line.get('style')
198+
family_name = line.get('family', current_app.config['LABEL_DEFAULT_FONT_FAMILY'])
199+
style_name = line.get('style', current_app.config['LABEL_DEFAULT_FONT_STYLE'])
200200
if family_name not in FONTS.fonts:
201201
raise LookupError("Unknown font family: %s" % family_name)
202-
if style_name not in FONTS.fonts[family_name]:
203-
style_name = current_app.config['LABEL_DEFAULT_FONT_STYLE']
204202
if style_name not in FONTS.fonts[family_name]:
205203
raise LookupError("Unknown font style: %s for font %s" %
206204
(style_name, family_name))
@@ -268,10 +266,6 @@ def get_uploaded_image(image):
268266

269267
# For each line in text, we determine and add the font path
270268
for line in context['text']:
271-
if 'family' not in line:
272-
line['family'] = current_app.config['LABEL_DEFAULT_FONT_FAMILY']
273-
if 'style' not in line:
274-
line['style'] = current_app.config['LABEL_DEFAULT_FONT_STYLE']
275269
if 'size' not in line or not line['size'].isdigit():
276270
raise ValueError("Font size is required")
277271
if int(line['size']) < 1:

app/labeldesigner/templates/labeldesigner.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,26 @@
105105
</div>
106106

107107
<label for="lineSpacing" class="control-label input-group" style="margin-top: 10px; margin-bottom: 0">Line Spacing:</label>
108-
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
108+
<div class="btn-group btn-group-toggle btn-block mb-3" data-toggle="buttons">
109109
{% for line_spacing in line_spacings %}
110110
<label class="btn btn-secondary {% if default_line_spacing == line_spacing %}active{% endif %}" id="lineSpacing{{line_spacing}}">
111111
<input type="radio" name="lineSpacing" onchange="preview()" value="{{line_spacing}}" aria-label="{{line_spacing}}%" {% if default_line_spacing == line_spacing %}checked{% endif %}>{{line_spacing}}%
112112
</label>
113113
{% endfor %}
114114
</div>
115+
<h6>Additional Font Options:</h6>
115116
<div class="form-check mb-2">
116117
<input class="form-check-input" type="checkbox" onchange="preview()" id="fontInverted" data-default="0">
117118
<label class="form-check-label" for="fontInverted">
118119
Invert text
119120
</label>
120121
</div>
122+
<div class="form-check mb-2">
123+
<input class="form-check-input" type="checkbox" onchange="preview()" id="fontCheckbox" data-default="0">
124+
<label class="form-check-label" for="fontCheckbox">
125+
TODO item (checkbox)
126+
</label>
127+
</div>
121128
<div class="red-support">
122129
<label for="printColor" class="control-label input-group" style="margin-top: 10px; margin-bottom: 0">Print
123130
Color:</label>
@@ -385,6 +392,9 @@
385392
<span class="fas fa-terminal" aria-hidden="true" style="margin-right: 0.3em"></span> Status
386393
<span id="statusIcon" class="float-right fas fa-exclamation-triangle" aria-hidden="true"></span>
387394
</div>
395+
<div id="printerStatusPanel" class="card-body" style="display: none;">
396+
<div id="printerStatusBox" class="alert alert-secondary" role="alert"></div>
397+
</div>
388398
<div id="statusPanel" class="card-body">
389399
<div id="statusBox" class="alert alert-secondary" role="alert"><span>Idle...</span></div>
390400
</div>

app/static/js/main.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function setFontSettingsPerLine() {
2222
style: $('#fontStyle option:selected').text(),
2323
size: $('#fontSize').val(),
2424
inverted: $('#fontInverted').is(':checked'),
25+
todo: $('#fontCheckbox').is(':checked'),
2526
align: $('input[name=fontAlign]:checked').val(),
2627
line_spacing: $('input[name=lineSpacing]:checked').val(),
2728
color: $('input[name=fontColor]:checked').val()
@@ -115,6 +116,8 @@ $(document).ready(function () {
115116
// Set font color
116117
$('input[name=fontColor]').prop('checked', false).parent().removeClass('active');
117118
$('input[name=fontColor][value="' + fs.color + '"]').prop('checked', true).parent().addClass('active');
119+
// Set TODO item
120+
$('#fontCheckbox').prop('checked', fs.todo);
118121
});
119122

120123
// When the user changes the caret/selection in the textarea, update #lineSelect and font controls
@@ -210,8 +213,7 @@ function gen_label(preview = true, cut_once = false) {
210213
// Check label against installed label in the printer
211214
updatePrinterStatus();
212215

213-
if(preview)
214-
{
216+
if (preview) {
215217
// Update preview image based on label size
216218
if ($('#labelSize option:selected').data('round') == 'True') {
217219
$('img#previewImg').addClass('roundPreviewImage');
@@ -283,7 +285,7 @@ function setStatus(data, what = null) {
283285
// Status icon green checkmark
284286
$('#statusIcon').removeClass().addClass('float-right fas fa-check text-success');
285287
}
286-
else {
288+
else if (what !== null) {
287289
// We are currently busy preparing the preview / printing
288290
const what = data.hasOwnProperty('printing') ? "Printing" : "Generating preview";
289291
$('#statusPanel').html('<div id="statusBox" class="alert alert-info" role="alert"><i class="fas fa-hourglass-half"></i><span>' + what + '...</span></div>');
@@ -297,10 +299,12 @@ function setStatus(data, what = null) {
297299
$('#statusIcon').removeClass().addClass('float-right fas fa-print text-success');
298300
} else {
299301
extra_info = ''
300-
if('message' in data) {
302+
if ('message' in data) {
301303
extra_info = ':<br />' + data['message']
302304
}
303-
$('#statusPanel').html('<div id="statusBox" class="alert alert-warning" role="alert"><i class="fas fa-exclamation-triangle"></i><span>' + what + extra_info + '</span></div>');
305+
if (what !== null) {
306+
$('#statusPanel').html('<div id="statusBox" class="alert alert-warning" role="alert"><i class="fas fa-exclamation-triangle"></i><span>' + what + extra_info + '</span></div>');
307+
}
304308
// Status icon red exclamation
305309
$('#statusIcon').removeClass().addClass('float-right fas fa-exclamation-triangle text-danger');
306310
}
@@ -409,7 +413,7 @@ function updatePrinterStatus() {
409413
if ($('#labelSize option:selected').val().includes('red')) {
410414
$(".red-support").show();
411415
} else {
412-
$('#print_color_black').prop('active', true);
416+
$('#print_color_black').prop('active', true);
413417
$(".red-support").hide();
414418
}
415419

@@ -445,7 +449,7 @@ function updatePrinterStatus() {
445449

446450
if (printer_status.errors && printer_status.errors.length > 0) {
447451
setStatus({ 'success': false })
448-
const printerErrors = document.getElementById('statusBox');
452+
const printerErrors = document.getElementById('printerStatusBox');
449453
if (printerErrors) {
450454
printerErrors.innerHTML = '';
451455
printer_status.errors.forEach((error) => {
@@ -456,6 +460,14 @@ function updatePrinterStatus() {
456460
printerErrors.parentElement.style.display = '';
457461
}
458462
}
463+
else {
464+
// Clear printer errors
465+
const printerErrors = document.getElementById('printerStatusBox');
466+
if (printerErrors) {
467+
printerErrors.innerHTML = '';
468+
printerErrors.parentElement.style.display = 'none';
469+
}
470+
}
459471
}
460472

461473
function getPrinterStatus() {
@@ -472,7 +484,7 @@ const LS_KEY = 'labeldesigner_settings_v1';
472484
function saveAllSettingsToLocalStorage() {
473485
const data = {};
474486
// Save all input/select/textarea values
475-
$('input, select, textarea').each(function() {
487+
$('input, select, textarea').each(function () {
476488
const key = this.id.length > 0 ? this.id : this.name;
477489
if (key.length == 0) return;
478490
if (this.type === 'checkbox') {
@@ -498,7 +510,7 @@ function restoreAllSettingsFromLocalStorage() {
498510
if (!raw) return;
499511
let data;
500512
try { data = JSON.parse(raw); } catch { return; }
501-
$('input, select, textarea').each(function() {
513+
$('input, select, textarea').each(function () {
502514
const key = this.id || this.name;
503515
if (!(key in data)) return;
504516
if (this.type === 'checkbox' || this.type === 'radio') {
@@ -515,15 +527,15 @@ function restoreAllSettingsFromLocalStorage() {
515527
try {
516528
window.fontSettingsPerLine = JSON.parse(data['fontSettingsPerLine']);
517529
$('#lineSelect').val(0).trigger('change');
518-
} catch {}
530+
} catch { }
519531
}
520532
// Trigger preview after restore
521533
setTimeout(() => { if (typeof preview === 'function') preview(); }, 100);
522534
}
523535

524536
function exportSettings() {
525537
const data = localStorage.getItem(LS_KEY) || '{}';
526-
const blob = new Blob([data], {type: 'application/json'});
538+
const blob = new Blob([data], { type: 'application/json' });
527539
const url = URL.createObjectURL(blob);
528540
const a = document.createElement('a');
529541
a.href = url;
@@ -537,15 +549,15 @@ function importSettings() {
537549
const input = document.createElement('input');
538550
input.type = 'file';
539551
input.accept = 'application/json';
540-
input.onchange = function(e) {
552+
input.onchange = function (e) {
541553
const file = e.target.files[0];
542554
if (!file) return;
543555
const reader = new FileReader();
544-
reader.onload = function(evt) {
556+
reader.onload = function (evt) {
545557
try {
546558
localStorage.setItem(LS_KEY, evt.target.result);
547559
restoreAllSettingsFromLocalStorage();
548-
} catch {}
560+
} catch { }
549561
};
550562
reader.readAsText(file);
551563
};
@@ -564,7 +576,7 @@ function resetSettings() {
564576

565577
function set_all_inputs_default(force = false) {
566578
// Iterate over those <input> that have a data-default propery and set the value if empty
567-
$('input[data-default], select[data-default], textarea[data-default]').each(function() {
579+
$('input[data-default], select[data-default], textarea[data-default]').each(function () {
568580
if (this.type === 'checkbox' || this.type === 'radio') {
569581
$(this).prop('checked', $(this).data('default') == 1 || $(this).data('default') === true);
570582
}
@@ -589,7 +601,7 @@ window.onload = function () {
589601
restoreAllSettingsFromLocalStorage();
590602

591603
// Save on change
592-
$(document).on('change input', 'input, select, textarea', function() {
604+
$(document).on('change input', 'input, select, textarea', function () {
593605
saveAllSettingsToLocalStorage();
594606
});
595607
// Export/Import/Reset buttons

screenshots/image8.png

271 KB
Loading

0 commit comments

Comments
 (0)