From e4a7679bc73cde4feba2b6254b695ecda98551d3 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:25:03 +0200 Subject: [PATCH] Add automatic label length trimming --- README.md | 4 +++- app/app.py | 29 +++++++++++++++++++++++++++++ app/static/editor.js | 26 +++++++++++++++++++++++++- app/static/mobile.js | 27 +++++++++++++++++++++++++-- app/templates/index.html | 4 ++++ app/templates/mobile.html | 4 ++++ 6 files changed, 90 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3591192..b0fee2c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Kleine Flask-Weboberflaeche zum Erstellen von Etiketten und Drucken auf einem Br Die Weboberflaeche bildet die wichtigsten P-touch-Editor-Funktionen nach: -- Bandbreite, Laenge, Rand und Ausrichtung +- Bandbreite, automatische Laenge, Rand und Ausrichtung - frei positionierbare Objekte auf einem Etiketten-Canvas - Textfelder, Symbole, Rahmen/Formen, Linien und Tabellen - Bildimport mit Schwarz/Weiss-Konvertierung @@ -108,6 +108,8 @@ Wenn `ptouch-print` auf deinem Setup andere Optionen braucht, nur `PRINT_COMMAND Hinweis: Der PT-P700 unterstuetzt laut Brother Auto Cut und Kettendruck, aber keinen Halbschnitt. Die Option `Kettendruck / Streifen` rendert mehrere Kopien als ein langes Druckbild, damit keine Vollschnitte zwischen den Labels entstehen. +Die automatische Laenge entfernt leeres Band am Ende des Etiketts. Links bzw. oben bleibt das Layout unveraendert, damit die Positionen im Editor nachvollziehbar bleiben. + ## USB testen Im laufenden Container: diff --git a/app/app.py b/app/app.py index 7868da3..ce06a06 100644 --- a/app/app.py +++ b/app/app.py @@ -223,6 +223,33 @@ def draw_cutmark(draw: ImageDraw.ImageDraw, width_px: int, height_px: int, scale y += dash + gap +def crop_label_to_content(image: Image.Image, design: dict[str, Any], scale: float) -> Image.Image: + if not bool(design.get("autoLength", True)): + return image + + tape_width = float(design.get("tapeWidthMm", 24)) + orientation = design.get("orientation", "landscape") + margin_px = scaled_mm_to_px(float(design.get("marginMm", 2)), scale) + min_length_px = scaled_mm_to_px(10, scale) + black_mask = image.convert("L").point(lambda pixel: 255 if pixel < 128 else 0, "L") + bbox = black_mask.getbbox() + + if orientation == "portrait": + fixed_width = scaled_mm_to_px(tape_width, scale) + if not bbox: + return new_label_image(fixed_width, min_length_px) + end = min(image.height, bbox[3] + margin_px) + target = min(image.height, max(min_length_px, end)) + return image.crop((0, 0, image.width, target)) + + fixed_height = scaled_mm_to_px(tape_width, scale) + if not bbox: + return new_label_image(min_length_px, fixed_height) + end = min(image.width, bbox[2] + margin_px) + target = min(image.width, max(min_length_px, end)) + return image.crop((0, 0, target, image.height)) + + def render_design_image(design: dict[str, Any]) -> Image.Image: tape_width = float(design.get("tapeWidthMm", 24)) if tape_width not in TAPE_WIDTHS_MM: @@ -264,6 +291,8 @@ def render_design_image(design: dict[str, Any]) -> Image.Image: elif obj_type == "barcode": draw_barcode(image, obj, scale) + image = crop_label_to_content(image, design, scale) + draw = ImageDraw.Draw(image) if design.get("cutMode") in {"cutmark", "cutmark-chain"}: draw_cutmark(draw, image.width, image.height, scale) diff --git a/app/static/editor.js b/app/static/editor.js index e9f4f2e..a6cdd73 100644 --- a/app/static/editor.js +++ b/app/static/editor.js @@ -16,6 +16,7 @@ const state = { design: { tapeWidthMm: 24, lengthMm: 70, + autoLength: true, marginMm: 2, orientation: "landscape", cutMode: "auto", @@ -31,6 +32,7 @@ const message = document.getElementById("message"); const controls = { tapeWidth: document.getElementById("tapeWidth"), lengthMm: document.getElementById("lengthMm"), + autoLength: document.getElementById("autoLength"), marginMm: document.getElementById("marginMm"), cutMode: document.getElementById("cutMode"), copies: document.getElementById("copies"), @@ -80,6 +82,21 @@ function printablePxPerMm() { return (PRINTABLE_TAPE_PX[tape] || (tape * 180) / 25.4) / tape; } +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function contentLengthMm() { + if (!state.design.objects.length) return Number(controls.lengthMm.value) || state.design.lengthMm || 70; + const margin = Number(controls.marginMm.value) || 0; + const lengthEnd = state.design.objects.reduce((max, obj) => { + if (obj.hidden) return max; + const end = state.design.orientation === "portrait" ? obj.y + obj.h : obj.x + obj.w; + return Math.max(max, end); + }, margin); + return Math.ceil(clamp(lengthEnd + margin, 10, 500)); +} + function selected() { return state.design.objects.find((obj) => obj.id === state.selectedId) || null; } @@ -94,13 +111,20 @@ function syncDesignControls() { state.design.lengthMm = Number(controls.lengthMm.value); state.design.marginMm = Number(controls.marginMm.value); state.design.cutMode = controls.cutMode.value; + state.design.autoLength = controls.autoLength.checked; state.design.halfcut = "off"; state.design.orientation = document.querySelector("input[name='orientation']:checked").value; + controls.lengthMm.disabled = state.design.autoLength; + if (state.design.autoLength) { + state.design.lengthMm = contentLengthMm(); + controls.lengthMm.value = state.design.lengthMm; + } } function applyDesignControls() { controls.tapeWidth.value = state.design.tapeWidthMm; controls.lengthMm.value = state.design.lengthMm; + controls.autoLength.checked = state.design.autoLength !== false; controls.marginMm.value = state.design.marginMm; controls.cutMode.value = state.design.cutMode || "auto"; document.querySelector(`input[name='orientation'][value='${state.design.orientation}']`).checked = true; @@ -411,7 +435,7 @@ window.addEventListener("keydown", (event) => { } }); -["tapeWidth", "lengthMm", "marginMm", "cutMode"].forEach((id) => document.getElementById(id).addEventListener("input", draw)); +["tapeWidth", "lengthMm", "autoLength", "marginMm", "cutMode"].forEach((id) => document.getElementById(id).addEventListener("input", draw)); document.querySelectorAll("input[name='orientation']").forEach((input) => input.addEventListener("change", draw)); Object.values(controls).forEach((control) => { if (control && control.id && control.id.startsWith("prop")) control.addEventListener("input", applyProps); diff --git a/app/static/mobile.js b/app/static/mobile.js index 4e3309e..590d4a2 100644 --- a/app/static/mobile.js +++ b/app/static/mobile.js @@ -2,6 +2,7 @@ const fields = { text: document.getElementById("mobileText"), tape: document.getElementById("mobileTape"), length: document.getElementById("mobileLength"), + autoLength: document.getElementById("mobileAutoLength"), margin: document.getElementById("mobileMargin"), font: document.getElementById("mobileFont"), copies: document.getElementById("mobileCopies"), @@ -13,6 +14,13 @@ const fields = { message: document.getElementById("mobileMessage"), }; +function estimateTextLengthMm(text, fontSize, margin, frame) { + const lines = String(text || " ").split("\n"); + const longest = lines.reduce((max, line) => Math.max(max, line.length), 0); + const textWidth = Math.max(8, longest * fontSize * 0.62); + return Math.min(300, Math.max(20, Math.ceil(textWidth + margin * 2 + (frame ? 4 : 0)))); +} + function setMessage(text, type = "") { fields.message.textContent = text; fields.message.className = `message ${type}`; @@ -20,9 +28,15 @@ function setMessage(text, type = "") { function mobileDesign() { const tapeWidth = Number(fields.tape.value); - const length = Number(fields.length.value); const margin = Number(fields.margin.value); const text = fields.text.value.trim() || " "; + const fontSize = Number(fields.font.value); + const autoLength = fields.autoLength.checked; + const length = autoLength + ? estimateTextLengthMm(text, fontSize, margin, fields.frame.checked) + : Number(fields.length.value); + fields.length.value = String(length); + fields.length.disabled = autoLength; const objects = []; if (fields.frame.checked) { objects.push({ @@ -44,7 +58,7 @@ function mobileDesign() { w: Math.max(2, length - margin * 2 - (fields.frame.checked ? 2 : 0)), h: Math.max(2, tapeWidth - margin * 2), text, - fontSizeMm: Number(fields.font.value), + fontSizeMm: fontSize, bold: fields.bold.checked, align: fields.align.value, valign: "middle", @@ -52,6 +66,7 @@ function mobileDesign() { return { tapeWidthMm: tapeWidth, lengthMm: length, + autoLength, marginMm: margin, orientation: "landscape", cutMode: fields.cutMode.value, @@ -89,3 +104,11 @@ document.getElementById("mobileDetect").addEventListener("click", async () => { fields.tape.value = String(payload.width); setMessage(`Band erkannt: ${payload.width} mm`, "success"); }); + +["text", "font", "margin", "frame", "autoLength"].forEach((name) => { + fields[name].addEventListener("input", () => { + if (name === "autoLength" || fields.autoLength.checked) mobileDesign(); + }); +}); + +mobileDesign(); diff --git a/app/templates/index.html b/app/templates/index.html index c9fabd0..146ab43 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -56,6 +56,10 @@ Laenge + +