diff --git a/README.md b/README.md index 5405a1a..2250e44 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Die Weboberflaeche bildet die wichtigsten P-touch-Editor-Funktionen nach: - QR-Code und Code128-Barcode - Layout speichern/laden im Browser - serverseitige Vorschau und Druck ueber `ptouch-print` +- Band-Erkennung mit Breite, Bandtyp, Hintergrundfarbe und Schriftfarbe - einfache Text-only Mobile-Webansicht unter `/mobile` - Schnittmodi: normal, Kettendruck/Streifen, Schnittmarke und Streifen mit Schnittmarke diff --git a/app/app.py b/app/app.py index ce06a06..515a33c 100644 --- a/app/app.py +++ b/app/app.py @@ -375,7 +375,76 @@ def should_render_copies_as_strip(design: dict[str, Any], copies: int) -> bool: return str(design.get("cutMode", "auto")) in {"chain", "cutmark-chain"} -def detect_tape_width() -> tuple[float | None, str]: +COLOR_HEX_BY_NAME = { + "white": "#ffffff", + "black": "#111111", + "red": "#d92d20", + "blue": "#2563eb", + "yellow": "#facc15", + "green": "#16a34a", + "clear": "#f8fafc", + "transparent": "#f8fafc", + "silver": "#c0c0c0", + "gold": "#d4af37", + "matte white": "#f5f5f0", + "matte black": "#1f1f1f", +} + + +COLOR_NAME_DE = { + "white": "Weiss", + "black": "Schwarz", + "red": "Rot", + "blue": "Blau", + "yellow": "Gelb", + "green": "Gruen", + "clear": "Transparent", + "transparent": "Transparent", + "silver": "Silber", + "gold": "Gold", + "matte white": "Matt weiss", + "matte black": "Matt schwarz", +} + + +MEDIA_TYPE_DE = { + "laminated tape": "Laminiertes Band", + "non-laminated tape": "Nicht laminiertes Band", + "fabric tape": "Textilband", + "heat shrink tube": "Schrumpfschlauch", +} + + +def color_info_from_line(output: str, key: str) -> dict[str, str] | None: + match = re.search(rf"^{re.escape(key)}\s*=\s*([0-9a-fA-F]+)(?:\s*\(([^)]+)\))?", output, re.MULTILINE) + if not match: + return None + code = match.group(1) + raw_name = (match.group(2) or f"Code {code}").strip() + normalized = raw_name.lower() + return { + "code": code, + "name": COLOR_NAME_DE.get(normalized, raw_name), + "rawName": raw_name, + "css": COLOR_HEX_BY_NAME.get(normalized, "#d1d5db"), + } + + +def media_type_from_output(output: str) -> dict[str, str] | None: + match = re.search(r"^media type\s*=\s*([0-9a-fA-F]+)(?:\s*\(([^)]+)\))?", output, re.MULTILINE) + if not match: + return None + code = match.group(1) + raw_name = (match.group(2) or f"Code {code}").strip() + normalized = raw_name.lower() + return { + "code": code, + "name": MEDIA_TYPE_DE.get(normalized, raw_name), + "rawName": raw_name, + } + + +def detect_tape_info() -> dict[str, Any]: result = subprocess.run( ["ptouch-print", "--info"], capture_output=True, @@ -387,6 +456,7 @@ def detect_tape_width() -> tuple[float | None, str]: if result.returncode != 0: raise RuntimeError(format_print_error(result)) + width = None candidates: list[float] = [] for pattern in [ r"(?:tape|media|band|width|breite|size|groesse|printing)[^\n:=-]*[:=-]?\s*(\d+(?:[.,]\d+)?)\s*mm", @@ -397,8 +467,16 @@ def detect_tape_width() -> tuple[float | None, str]: for supported in TAPE_WIDTHS_MM: if any(abs(candidate - supported) < 0.26 for candidate in candidates): - return supported, output - return None, output + width = supported + break + + return { + "width": width, + "mediaType": media_type_from_output(output), + "tapeColor": color_info_from_line(output, "tape color"), + "textColor": color_info_from_line(output, "text color"), + "raw": output, + } def format_print_error(result: subprocess.CompletedProcess[str]) -> str: @@ -503,10 +581,10 @@ def api_print(): @app.get("/api/tape-info") def api_tape_info(): try: - width, raw = detect_tape_width() - if width is None: - return jsonify({"ok": False, "error": "Bandbreite konnte nicht erkannt werden.", "raw": raw}), 400 - return jsonify({"ok": True, "width": width, "raw": raw}) + info = detect_tape_info() + if info["width"] is None: + return jsonify({"ok": False, "error": "Bandbreite konnte nicht erkannt werden.", **info}), 400 + return jsonify({"ok": True, **info}) except Exception as exc: return jsonify({"ok": False, "error": str(exc)}), 400 diff --git a/app/static/editor.js b/app/static/editor.js index a6cdd73..a460fbd 100644 --- a/app/static/editor.js +++ b/app/static/editor.js @@ -13,6 +13,7 @@ const state = { zoom: 1.5, selectedId: null, dragging: null, + tapeInfo: null, design: { tapeWidthMm: 24, lengthMm: 70, @@ -36,6 +37,12 @@ const controls = { marginMm: document.getElementById("marginMm"), cutMode: document.getElementById("cutMode"), copies: document.getElementById("copies"), + tapeInfo: document.getElementById("tapeInfo"), + tapeInfoType: document.getElementById("tapeInfoType"), + tapeBgSwatch: document.getElementById("tapeBgSwatch"), + tapeBgText: document.getElementById("tapeBgText"), + tapeTextSwatch: document.getElementById("tapeTextSwatch"), + tapeTextText: document.getElementById("tapeTextText"), zoomLabel: document.getElementById("zoomLabel"), previewImage: document.getElementById("previewImage"), noSelection: document.getElementById("noSelection"), @@ -82,6 +89,10 @@ function printablePxPerMm() { return (PRINTABLE_TAPE_PX[tape] || (tape * 180) / 25.4) / tape; } +function inkColor() { + return state.tapeInfo?.textColor?.css || "#000"; +} + function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } @@ -106,6 +117,20 @@ function setMessage(text, type = "") { message.className = `message-line ${type}`; } +function updateTapeInfo(info) { + state.tapeInfo = info || null; + controls.tapeInfo.hidden = !info; + if (!info) return; + const media = info.mediaType?.name || "Unbekannt"; + const bg = info.tapeColor || { name: "Unbekannt", css: "#d1d5db" }; + const text = info.textColor || { name: "Unbekannt", css: "#111111" }; + controls.tapeInfoType.textContent = `${info.width} mm · ${media}${info.mediaType?.code ? ` (${info.mediaType.code})` : ""}`; + controls.tapeBgText.textContent = `${bg.name}${bg.code ? ` (${bg.code})` : ""}`; + controls.tapeBgSwatch.style.background = bg.css; + controls.tapeTextText.textContent = `${text.name}${text.code ? ` (${text.code})` : ""}`; + controls.tapeTextSwatch.style.background = text.css; +} + function syncDesignControls() { state.design.tapeWidthMm = Number(controls.tapeWidth.value); state.design.lengthMm = Number(controls.lengthMm.value); @@ -164,7 +189,7 @@ function drawTextObject(obj) { ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip(); - ctx.fillStyle = "#000"; + ctx.fillStyle = inkColor(); const fontPx = (obj.fontSizeMm || 5) * printablePxPerMm() * state.zoom; ctx.font = `${obj.bold ? "700 " : ""}${fontPx}px DejaVu Sans, Arial, system-ui, sans-serif`; ctx.textBaseline = "middle"; @@ -188,8 +213,8 @@ function drawObject(obj) { const h = mmToPx(obj.h); ctx.save(); ctx.lineWidth = Math.max(1, mmToPx(obj.lineMm || 0.25)); - ctx.strokeStyle = "#000"; - ctx.fillStyle = "#000"; + ctx.strokeStyle = inkColor(); + ctx.fillStyle = inkColor(); if (obj.type === "text" || obj.type === "symbol") { drawTextObject(obj); @@ -250,7 +275,8 @@ function draw() { canvas.style.width = `${canvas.width}px`; canvas.style.height = `${canvas.height}px`; - ctx.fillStyle = "#fff"; + const tapeColor = state.tapeInfo?.tapeColor?.css || "#fff"; + ctx.fillStyle = tapeColor; ctx.fillRect(0, 0, canvas.width, canvas.height); const margin = mmToPx(state.design.marginMm); ctx.strokeStyle = "#b7b7b7"; @@ -261,7 +287,7 @@ function draw() { for (const obj of state.design.objects) drawObject(obj); if (state.design.cutMode === "cutmark" || state.design.cutMode === "cutmark-chain") { - ctx.strokeStyle = "#111"; + ctx.strokeStyle = inkColor(); ctx.setLineDash([4, 4]); const x = canvas.width - mmToPx(1.5); ctx.beginPath(); @@ -477,8 +503,11 @@ document.getElementById("detectTape").addEventListener("click", async () => { } controls.tapeWidth.value = String(payload.width); state.design.tapeWidthMm = Number(payload.width); + updateTapeInfo(payload); draw(); - setMessage(`Band erkannt: ${payload.width} mm`, "success"); + const bg = payload.tapeColor?.name ? `, ${payload.tapeColor.name}` : ""; + const text = payload.textColor?.name ? ` / ${payload.textColor.name}` : ""; + setMessage(`Band erkannt: ${payload.width} mm${bg}${text}`, "success"); }); document.getElementById("saveLayout").addEventListener("click", () => { diff --git a/app/static/mobile.css b/app/static/mobile.css index b889d05..62779a9 100644 --- a/app/static/mobile.css +++ b/app/static/mobile.css @@ -174,6 +174,42 @@ button.primary { line-height: 1.35; } +.tape-info { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid #444; + border-radius: 7px; + background: #2f2f2f; +} + +.tape-info div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.tape-info span { + color: #b7b7b7; +} + +.tape-info strong { + display: inline-flex; + align-items: center; + gap: 7px; + color: #f2f2f2; + text-align: right; +} + +.tape-info i { + width: 18px; + height: 18px; + border: 1px solid #777; + border-radius: 4px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); +} + .bottom-actions { position: fixed; left: 0; diff --git a/app/static/mobile.js b/app/static/mobile.js index 02567cc..471d48d 100644 --- a/app/static/mobile.js +++ b/app/static/mobile.js @@ -7,6 +7,12 @@ const fields = { align: document.getElementById("mobileAlign"), bold: document.getElementById("mobileBold"), frame: document.getElementById("mobileFrame"), + tapeInfo: document.getElementById("mobileTapeInfo"), + tapeInfoType: document.getElementById("mobileTapeInfoType"), + tapeBgSwatch: document.getElementById("mobileTapeBgSwatch"), + tapeBgText: document.getElementById("mobileTapeBgText"), + tapeTextSwatch: document.getElementById("mobileTapeTextSwatch"), + tapeTextText: document.getElementById("mobileTapeTextText"), preview: document.getElementById("mobilePreview"), message: document.getElementById("mobileMessage"), }; @@ -23,6 +29,19 @@ function setMessage(text, type = "") { fields.message.className = `message ${type}`; } +function updateTapeInfo(info) { + fields.tapeInfo.hidden = !info; + if (!info) return; + const media = info.mediaType?.name || "Unbekannt"; + const bg = info.tapeColor || { name: "Unbekannt", css: "#d1d5db" }; + const text = info.textColor || { name: "Unbekannt", css: "#111111" }; + fields.tapeInfoType.textContent = `${info.width} mm · ${media}${info.mediaType?.code ? ` (${info.mediaType.code})` : ""}`; + fields.tapeBgText.textContent = `${bg.name}${bg.code ? ` (${bg.code})` : ""}`; + fields.tapeBgSwatch.style.background = bg.css; + fields.tapeTextText.textContent = `${text.name}${text.code ? ` (${text.code})` : ""}`; + fields.tapeTextSwatch.style.background = text.css; +} + function mobileDesign() { const tapeWidth = Number(fields.tape.value); const margin = Number(fields.margin.value); @@ -94,7 +113,10 @@ document.getElementById("mobileDetect").addEventListener("click", async () => { return; } fields.tape.value = String(payload.width); - setMessage(`Band erkannt: ${payload.width} mm`, "success"); + updateTapeInfo(payload); + const bg = payload.tapeColor?.name ? `, ${payload.tapeColor.name}` : ""; + const text = payload.textColor?.name ? ` / ${payload.textColor.name}` : ""; + setMessage(`Band erkannt: ${payload.width} mm${bg}${text}`, "success"); }); ["text", "font", "margin", "frame", "bold", "align", "tape"].forEach((name) => { diff --git a/app/static/style.css b/app/static/style.css index 9345abf..8ddf6c1 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -163,6 +163,42 @@ textarea { line-height: 1.35; } +.tape-info { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid #444; + border-radius: 7px; + background: #2f2f2f; +} + +.tape-info div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.tape-info span { + color: #b7b7b7; +} + +.tape-info strong { + display: inline-flex; + align-items: center; + gap: 7px; + color: #f2f2f2; + text-align: right; +} + +.tape-info i { + width: 18px; + height: 18px; + border: 1px solid #777; + border-radius: 4px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); +} + .sidebar label { display: grid; gap: 6px; diff --git a/app/templates/index.html b/app/templates/index.html index 146ab43..e45d9e4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -52,6 +52,20 @@ +