Add automatic label length trimming
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user