Add automatic label length trimming

This commit is contained in:
Mikei386
2026-06-22 13:25:03 +02:00
parent 941c68bbf8
commit e4a7679bc7
6 changed files with 90 additions and 4 deletions
+3 -1
View File
@@ -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: 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 - frei positionierbare Objekte auf einem Etiketten-Canvas
- Textfelder, Symbole, Rahmen/Formen, Linien und Tabellen - Textfelder, Symbole, Rahmen/Formen, Linien und Tabellen
- Bildimport mit Schwarz/Weiss-Konvertierung - 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. 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 ## USB testen
Im laufenden Container: Im laufenden Container:
+29
View File
@@ -223,6 +223,33 @@ def draw_cutmark(draw: ImageDraw.ImageDraw, width_px: int, height_px: int, scale
y += dash + gap 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: def render_design_image(design: dict[str, Any]) -> Image.Image:
tape_width = float(design.get("tapeWidthMm", 24)) tape_width = float(design.get("tapeWidthMm", 24))
if tape_width not in TAPE_WIDTHS_MM: 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": elif obj_type == "barcode":
draw_barcode(image, obj, scale) 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"}: if design.get("cutMode") in {"cutmark", "cutmark-chain"}:
draw_cutmark(draw, image.width, image.height, scale) draw_cutmark(draw, image.width, image.height, scale)
+25 -1
View File
@@ -16,6 +16,7 @@ const state = {
design: { design: {
tapeWidthMm: 24, tapeWidthMm: 24,
lengthMm: 70, lengthMm: 70,
autoLength: true,
marginMm: 2, marginMm: 2,
orientation: "landscape", orientation: "landscape",
cutMode: "auto", cutMode: "auto",
@@ -31,6 +32,7 @@ const message = document.getElementById("message");
const controls = { const controls = {
tapeWidth: document.getElementById("tapeWidth"), tapeWidth: document.getElementById("tapeWidth"),
lengthMm: document.getElementById("lengthMm"), lengthMm: document.getElementById("lengthMm"),
autoLength: document.getElementById("autoLength"),
marginMm: document.getElementById("marginMm"), marginMm: document.getElementById("marginMm"),
cutMode: document.getElementById("cutMode"), cutMode: document.getElementById("cutMode"),
copies: document.getElementById("copies"), copies: document.getElementById("copies"),
@@ -80,6 +82,21 @@ function printablePxPerMm() {
return (PRINTABLE_TAPE_PX[tape] || (tape * 180) / 25.4) / tape; 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() { function selected() {
return state.design.objects.find((obj) => obj.id === state.selectedId) || null; 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.lengthMm = Number(controls.lengthMm.value);
state.design.marginMm = Number(controls.marginMm.value); state.design.marginMm = Number(controls.marginMm.value);
state.design.cutMode = controls.cutMode.value; state.design.cutMode = controls.cutMode.value;
state.design.autoLength = controls.autoLength.checked;
state.design.halfcut = "off"; state.design.halfcut = "off";
state.design.orientation = document.querySelector("input[name='orientation']:checked").value; 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() { function applyDesignControls() {
controls.tapeWidth.value = state.design.tapeWidthMm; controls.tapeWidth.value = state.design.tapeWidthMm;
controls.lengthMm.value = state.design.lengthMm; controls.lengthMm.value = state.design.lengthMm;
controls.autoLength.checked = state.design.autoLength !== false;
controls.marginMm.value = state.design.marginMm; controls.marginMm.value = state.design.marginMm;
controls.cutMode.value = state.design.cutMode || "auto"; controls.cutMode.value = state.design.cutMode || "auto";
document.querySelector(`input[name='orientation'][value='${state.design.orientation}']`).checked = true; 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)); document.querySelectorAll("input[name='orientation']").forEach((input) => input.addEventListener("change", draw));
Object.values(controls).forEach((control) => { Object.values(controls).forEach((control) => {
if (control && control.id && control.id.startsWith("prop")) control.addEventListener("input", applyProps); if (control && control.id && control.id.startsWith("prop")) control.addEventListener("input", applyProps);
+25 -2
View File
@@ -2,6 +2,7 @@ const fields = {
text: document.getElementById("mobileText"), text: document.getElementById("mobileText"),
tape: document.getElementById("mobileTape"), tape: document.getElementById("mobileTape"),
length: document.getElementById("mobileLength"), length: document.getElementById("mobileLength"),
autoLength: document.getElementById("mobileAutoLength"),
margin: document.getElementById("mobileMargin"), margin: document.getElementById("mobileMargin"),
font: document.getElementById("mobileFont"), font: document.getElementById("mobileFont"),
copies: document.getElementById("mobileCopies"), copies: document.getElementById("mobileCopies"),
@@ -13,6 +14,13 @@ const fields = {
message: document.getElementById("mobileMessage"), 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 = "") { function setMessage(text, type = "") {
fields.message.textContent = text; fields.message.textContent = text;
fields.message.className = `message ${type}`; fields.message.className = `message ${type}`;
@@ -20,9 +28,15 @@ function setMessage(text, type = "") {
function mobileDesign() { function mobileDesign() {
const tapeWidth = Number(fields.tape.value); const tapeWidth = Number(fields.tape.value);
const length = Number(fields.length.value);
const margin = Number(fields.margin.value); const margin = Number(fields.margin.value);
const text = fields.text.value.trim() || " "; 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 = []; const objects = [];
if (fields.frame.checked) { if (fields.frame.checked) {
objects.push({ objects.push({
@@ -44,7 +58,7 @@ function mobileDesign() {
w: Math.max(2, length - margin * 2 - (fields.frame.checked ? 2 : 0)), w: Math.max(2, length - margin * 2 - (fields.frame.checked ? 2 : 0)),
h: Math.max(2, tapeWidth - margin * 2), h: Math.max(2, tapeWidth - margin * 2),
text, text,
fontSizeMm: Number(fields.font.value), fontSizeMm: fontSize,
bold: fields.bold.checked, bold: fields.bold.checked,
align: fields.align.value, align: fields.align.value,
valign: "middle", valign: "middle",
@@ -52,6 +66,7 @@ function mobileDesign() {
return { return {
tapeWidthMm: tapeWidth, tapeWidthMm: tapeWidth,
lengthMm: length, lengthMm: length,
autoLength,
marginMm: margin, marginMm: margin,
orientation: "landscape", orientation: "landscape",
cutMode: fields.cutMode.value, cutMode: fields.cutMode.value,
@@ -89,3 +104,11 @@ document.getElementById("mobileDetect").addEventListener("click", async () => {
fields.tape.value = String(payload.width); fields.tape.value = String(payload.width);
setMessage(`Band erkannt: ${payload.width} mm`, "success"); 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();
+4
View File
@@ -56,6 +56,10 @@
Laenge Laenge
<input id="lengthMm" type="number" min="10" max="500" step="1" value="70"> <input id="lengthMm" type="number" min="10" max="500" step="1" value="70">
</label> </label>
<label class="check-row">
<input id="autoLength" type="checkbox" checked>
Laenge automatisch begrenzen
</label>
<label> <label>
Rand Rand
<input id="marginMm" type="number" min="0" max="20" step="0.5" value="2"> <input id="marginMm" type="number" min="0" max="20" step="0.5" value="2">
+4
View File
@@ -46,6 +46,10 @@
<input id="mobileMargin" type="number" min="0" max="20" step="0.5" value="2"> <input id="mobileMargin" type="number" min="0" max="20" step="0.5" value="2">
</label> </label>
</div> </div>
<label class="check-row">
<input id="mobileAutoLength" type="checkbox" checked>
Laenge automatisch begrenzen
</label>
<div class="grid-2"> <div class="grid-2">
<label> <label>