Add mobile mode and print cut controls

This commit is contained in:
Mikei386
2026-06-22 06:46:52 +02:00
parent 7029ee9519
commit a6025eef47
10 changed files with 303 additions and 48 deletions
+3 -1
View File
@@ -2,8 +2,10 @@ FROM alpine:3.20 AS ptouch-build
RUN apk add --no-cache build-base cmake gd-dev gettext-dev git libusb-dev pkgconf RUN apk add --no-cache build-base cmake gd-dev gettext-dev git libusb-dev pkgconf
RUN git clone --depth 1 --branch farix-main https://github.com/farixembedded/ptouch-print.git /src/ptouch RUN git clone --depth 1 --branch farix-main https://github.com/farixembedded/ptouch-print.git /src/ptouch
COPY patches/ptouch-print-cut-options.patch /tmp/ptouch-print-cut-options.patch
WORKDIR /src/ptouch WORKDIR /src/ptouch
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ RUN patch -p1 < /tmp/ptouch-print-cut-options.patch \
&& cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build \ && cmake --build build \
&& install -m 0755 build/ptouch-print /usr/local/bin/ptouch-print && install -m 0755 build/ptouch-print /usr/local/bin/ptouch-print
+4 -1
View File
@@ -12,6 +12,7 @@ Die Weboberflaeche bildet die wichtigsten P-touch-Editor-Funktionen nach:
- Layout speichern/laden im Browser - Layout speichern/laden im Browser
- serverseitige Vorschau und Druck ueber `ptouch-print` - serverseitige Vorschau und Druck ueber `ptouch-print`
- vereinfachte Mobile-Webansicht unter `/mobile` - vereinfachte Mobile-Webansicht unter `/mobile`
- Schnittmodi: normal, Halbschnitt ein/aus, Kettendruck, Schnittmarke, Kettendruck mit Schnittmarke
## Wichtig fuer den PT-P700 ## Wichtig fuer den PT-P700
@@ -103,7 +104,9 @@ Umgebungsvariablen:
- `LABEL_DPI=180` passt zum PT-P700. - `LABEL_DPI=180` passt zum PT-P700.
- `LABEL_DIR=/data/labels` speichert erzeugte PNG-Dateien. - `LABEL_DIR=/data/labels` speichert erzeugte PNG-Dateien.
Wenn `ptouch-print` auf deinem Setup andere Optionen braucht, nur `PRINT_COMMAND` anpassen. Der Platzhalter `{image}` wird von der App ersetzt. Wenn dein Kommando selbst Kopien unterstuetzt, kannst du zusaetzlich `{copies}` verwenden; sonst druckt die App mehrere Kopien durch mehrere Druckaufrufe. Wenn `ptouch-print` auf deinem Setup andere Optionen braucht, nur `PRINT_COMMAND` anpassen. Der Platzhalter `{image}` wird von der App ersetzt. Wenn dein Kommando selbst Kopien unterstuetzt, kannst du zusaetzlich `{copies}` verwenden; sonst druckt die App mehrere Kopien durch mehrere Druckaufrufe. Fuer Schnittoptionen kannst du optional `{cut_args}` im Kommando platzieren; ohne Platzhalter haengt die App die Schnittargumente automatisch an.
Hinweis: Der PT-P700-Halbschnitt ist in upstream `ptouch-print` nicht als CLI-Option vorhanden. Dieses Projekt patcht `ptouch-print` beim Containerstart um `--halfcut` und `--no-halfcut`.
## USB testen ## USB testen
+155 -36
View File
@@ -2,10 +2,12 @@ from __future__ import annotations
import base64 import base64
import io import io
import signal
import os import os
import re import re
import shlex import shlex
import subprocess import subprocess
import threading
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -34,17 +36,40 @@ DEFAULT_PRINT_COMMAND = "ptouch-print --image {image}"
PRINT_COMMAND = os.environ.get("PRINT_COMMAND", DEFAULT_PRINT_COMMAND) PRINT_COMMAND = os.environ.get("PRINT_COMMAND", DEFAULT_PRINT_COMMAND)
PRINT_ENABLED = os.environ.get("PRINT_ENABLED", "1").lower() in {"1", "true", "yes", "on"} PRINT_ENABLED = os.environ.get("PRINT_ENABLED", "1").lower() in {"1", "true", "yes", "on"}
DPI = int(os.environ.get("LABEL_DPI", "180")) DPI = int(os.environ.get("LABEL_DPI", "180"))
PTOUCH_SUPPORTS_HALFCUT: bool | None = None
TAPE_WIDTHS_MM = [3.5, 6, 9, 12, 18, 24] TAPE_WIDTHS_MM = [3.5, 6, 9, 12, 18, 24]
PRINTABLE_TAPE_PX = {
3.5: 24,
6.0: 32,
9.0: 52,
12.0: 76,
18.0: 120,
24.0: 128,
}
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "change-me") app.secret_key = os.environ.get("SECRET_KEY", "change-me")
PRINT_LOCK = threading.Lock()
def mm_to_px(mm: float) -> int: def mm_to_px(mm: float) -> int:
return max(1, round(mm / 25.4 * DPI)) return max(1, round(mm / 25.4 * DPI))
def printable_px_for_tape(tape_width_mm: float) -> int:
normalized = float(tape_width_mm)
return PRINTABLE_TAPE_PX.get(normalized, mm_to_px(normalized))
def render_scale_for_tape(tape_width_mm: float) -> float:
return printable_px_for_tape(tape_width_mm) / float(tape_width_mm)
def scaled_mm_to_px(mm: float, scale: float) -> int:
return max(1, round(mm * scale))
def clamp(value: float, low: float, high: float) -> float: def clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value)) return max(low, min(high, value))
@@ -67,11 +92,11 @@ def new_label_image(width_px: int, height_px: int) -> Image.Image:
return image return image
def object_box(obj: dict[str, Any]) -> tuple[int, int, int, int]: def object_box(obj: dict[str, Any], scale: float) -> tuple[int, int, int, int]:
x = mm_to_px(float(obj.get("x", 0))) x = scaled_mm_to_px(float(obj.get("x", 0)), scale)
y = mm_to_px(float(obj.get("y", 0))) y = scaled_mm_to_px(float(obj.get("y", 0)), scale)
w = mm_to_px(float(obj.get("w", 10))) w = scaled_mm_to_px(float(obj.get("w", 10)), scale)
h = mm_to_px(float(obj.get("h", 5))) h = scaled_mm_to_px(float(obj.get("h", 5)), scale)
return x, y, max(1, w), max(1, h) return x, y, max(1, w), max(1, h)
@@ -92,10 +117,10 @@ def wrap_text(draw: ImageDraw.ImageDraw, text: str, font, max_width: int) -> lis
return lines return lines
def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None: def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
text = str(obj.get("text", "Text")) text = str(obj.get("text", "Text"))
font_size = mm_to_px(float(obj.get("fontSizeMm", 5))) font_size = scaled_mm_to_px(float(obj.get("fontSizeMm", 5)), scale)
font = get_font(font_size, bool(obj.get("bold", False))) font = get_font(font_size, bool(obj.get("bold", False)))
align = obj.get("align", "left") align = obj.get("align", "left")
valign = obj.get("valign", "middle") valign = obj.get("valign", "middle")
@@ -122,9 +147,9 @@ def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
cy += line_h + gap cy += line_h + gap
def draw_shape(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None: def draw_shape(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
line = max(1, mm_to_px(float(obj.get("lineMm", 0.25)))) line = max(1, scaled_mm_to_px(float(obj.get("lineMm", 0.25)), scale))
shape = obj.get("shape", "rect") shape = obj.get("shape", "rect")
fill = 1 if obj.get("fill", False) else None fill = 1 if obj.get("fill", False) else None
box = [x, y, x + w, y + h] box = [x, y, x + w, y + h]
@@ -150,10 +175,10 @@ def decode_data_image(data_url: str) -> Image.Image:
return Image.open(io.BytesIO(base64.b64decode(data_url))) return Image.open(io.BytesIO(base64.b64decode(data_url)))
def draw_qr(image: Image.Image, obj: dict[str, Any]) -> None: def draw_qr(image: Image.Image, obj: dict[str, Any], scale: float) -> None:
if qrcode is None: if qrcode is None:
raise RuntimeError("qrcode ist nicht installiert.") raise RuntimeError("qrcode ist nicht installiert.")
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
qr = qrcode.QRCode(border=1, box_size=10) qr = qrcode.QRCode(border=1, box_size=10)
qr.add_data(str(obj.get("text", "https://example.local"))) qr.add_data(str(obj.get("text", "https://example.local")))
qr.make(fit=True) qr.make(fit=True)
@@ -162,10 +187,10 @@ def draw_qr(image: Image.Image, obj: dict[str, Any]) -> None:
paste_mono(image, qr_img, x, y, side, side) paste_mono(image, qr_img, x, y, side, side)
def draw_barcode(image: Image.Image, obj: dict[str, Any]) -> None: def draw_barcode(image: Image.Image, obj: dict[str, Any], scale: float) -> None:
if barcode is None or ImageWriter is None: if barcode is None or ImageWriter is None:
raise RuntimeError("python-barcode ist nicht installiert.") raise RuntimeError("python-barcode ist nicht installiert.")
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
writer = ImageWriter() writer = ImageWriter()
code = barcode.get("code128", str(obj.get("text", "1234567890")), writer=writer) code = barcode.get("code128", str(obj.get("text", "1234567890")), writer=writer)
buffer = io.BytesIO() buffer = io.BytesIO()
@@ -174,11 +199,11 @@ def draw_barcode(image: Image.Image, obj: dict[str, Any]) -> None:
paste_mono(image, Image.open(buffer), x, y, w, h) paste_mono(image, Image.open(buffer), x, y, w, h)
def draw_table(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None: def draw_table(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
rows = max(1, int(obj.get("rows", 2))) rows = max(1, int(obj.get("rows", 2)))
cols = max(1, int(obj.get("cols", 2))) cols = max(1, int(obj.get("cols", 2)))
line = max(1, mm_to_px(float(obj.get("lineMm", 0.2)))) line = max(1, scaled_mm_to_px(float(obj.get("lineMm", 0.2)), scale))
draw.rectangle([x, y, x + w, y + h], outline=1, width=line) draw.rectangle([x, y, x + w, y + h], outline=1, width=line)
for col in range(1, cols): for col in range(1, cols):
cx = x + round(w * col / cols) cx = x + round(w * col / cols)
@@ -188,6 +213,16 @@ def draw_table(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
draw.line((x, cy, x + w, cy), fill=1, width=line) draw.line((x, cy, x + w, cy), fill=1, width=line)
def draw_cutmark(draw: ImageDraw.ImageDraw, width_px: int, height_px: int, scale: float) -> None:
x = max(0, width_px - scaled_mm_to_px(1.5, scale))
dash = max(2, scaled_mm_to_px(1, scale))
gap = max(2, scaled_mm_to_px(0.7, scale))
y = 0
while y < height_px:
draw.line((x, y, x, min(height_px, y + dash)), fill=1, width=1)
y += dash + gap
def render_design(design: dict[str, Any]) -> Path: def render_design(design: dict[str, Any]) -> Path:
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:
@@ -199,7 +234,14 @@ def render_design(design: dict[str, Any]) -> Path:
else: else:
canvas_w_mm, canvas_h_mm = length, tape_width canvas_w_mm, canvas_h_mm = length, tape_width
image = new_label_image(mm_to_px(canvas_w_mm), mm_to_px(canvas_h_mm)) scale = render_scale_for_tape(tape_width)
image = new_label_image(scaled_mm_to_px(canvas_w_mm, scale), scaled_mm_to_px(canvas_h_mm, scale))
max_height_px = printable_px_for_tape(tape_width)
if image.height > max_height_px:
raise ValueError(
f"Gerendertes Etikett ist zu hoch ({image.height}px). "
f"Maximal fuer {tape_width:g} mm Band sind {max_height_px}px."
)
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
for obj in design.get("objects", []): for obj in design.get("objects", []):
@@ -207,35 +249,93 @@ def render_design(design: dict[str, Any]) -> Path:
continue continue
obj_type = obj.get("type") obj_type = obj.get("type")
if obj_type == "text" or obj_type == "symbol": if obj_type == "text" or obj_type == "symbol":
draw_text(draw, obj) draw_text(draw, obj, scale)
elif obj_type == "shape": elif obj_type == "shape":
draw_shape(draw, obj) draw_shape(draw, obj, scale)
elif obj_type == "line": elif obj_type == "line":
draw_shape(draw, {**obj, "shape": "line"}) draw_shape(draw, {**obj, "shape": "line"}, scale)
elif obj_type == "table": elif obj_type == "table":
draw_table(draw, obj) draw_table(draw, obj, scale)
elif obj_type == "image" and obj.get("dataUrl"): elif obj_type == "image" and obj.get("dataUrl"):
x, y, w, h = object_box(obj) x, y, w, h = object_box(obj, scale)
paste_mono(image, decode_data_image(str(obj["dataUrl"])), x, y, w, h) paste_mono(image, decode_data_image(str(obj["dataUrl"])), x, y, w, h)
elif obj_type == "qr": elif obj_type == "qr":
draw_qr(image, obj) draw_qr(image, obj, scale)
elif obj_type == "barcode": elif obj_type == "barcode":
draw_barcode(image, obj) draw_barcode(image, obj, scale)
if design.get("cutMode") in {"cutmark", "cutmark-chain"}:
draw_cutmark(draw, image.width, image.height, scale)
output = LABEL_DIR / f"label-{uuid.uuid4().hex}.png" output = LABEL_DIR / f"label-{uuid.uuid4().hex}.png"
image.save(output) image.save(output)
return output return output
def run_print_command(image_path: Path, copies: int) -> subprocess.CompletedProcess[str]: def ptouch_supports_halfcut() -> bool:
command = PRINT_COMMAND.format(image=shlex.quote(str(image_path)), copies=copies) global PTOUCH_SUPPORTS_HALFCUT
return subprocess.run(shlex.split(command), capture_output=True, check=False, text=True, timeout=90) if PTOUCH_SUPPORTS_HALFCUT is not None:
return PTOUCH_SUPPORTS_HALFCUT
result = subprocess.run(
["ptouch-print", "--help"],
capture_output=True,
check=False,
text=True,
timeout=10,
)
help_text = f"{result.stdout}\n{result.stderr}"
PTOUCH_SUPPORTS_HALFCUT = "--no-halfcut" in help_text and "--halfcut" in help_text
return PTOUCH_SUPPORTS_HALFCUT
def print_label(image_path: Path, copies: int) -> list[subprocess.CompletedProcess[str]]: def cut_args_for_design(design: dict[str, Any]) -> str:
args = []
if design.get("cutMode") in {"chain", "cutmark-chain"}:
args.append("--chain")
halfcut = design.get("halfcut", "on")
if halfcut == "off":
if not ptouch_supports_halfcut():
raise RuntimeError(
"Halbschnitt Aus braucht die gepatchte ptouch-print-Version. "
"Bitte start.sh und patches/ptouch-print-cut-options.patch nach Unraid kopieren, "
"ptouch-print im Container entfernen oder Container neu erstellen und danach neu starten."
)
args.append("--no-halfcut")
elif halfcut == "on" and ptouch_supports_halfcut():
args.append("--halfcut")
return " ".join(args)
def run_print_command(image_path: Path, copies: int, design: dict[str, Any]) -> subprocess.CompletedProcess[str]:
cut_args = cut_args_for_design(design)
command = PRINT_COMMAND.format(image=shlex.quote(str(image_path)), copies=copies, cut_args=cut_args)
if cut_args and "{cut_args}" not in PRINT_COMMAND:
command = f"{command} {cut_args}"
process = subprocess.Popen(
shlex.split(command),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
try:
stdout, stderr = process.communicate(timeout=90)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
stdout, stderr = process.communicate()
return subprocess.CompletedProcess(
shlex.split(command),
124,
stdout,
f"{stderr}\nDruckprozess wurde nach Timeout beendet.".strip(),
)
return subprocess.CompletedProcess(shlex.split(command), process.returncode, stdout, stderr)
def print_label(image_path: Path, copies: int, design: dict[str, Any]) -> list[subprocess.CompletedProcess[str]]:
if "{copies}" in PRINT_COMMAND: if "{copies}" in PRINT_COMMAND:
return [run_print_command(image_path, copies)] return [run_print_command(image_path, copies, design)]
return [run_print_command(image_path, 1) for _ in range(copies)] return [run_print_command(image_path, 1, design) for _ in range(copies)]
def detect_tape_width() -> tuple[float | None, str]: def detect_tape_width() -> tuple[float | None, str]:
@@ -278,6 +378,20 @@ def format_print_error(result: subprocess.CompletedProcess[str]) -> str:
"In Unraid pruefen: Privileged an, /dev/bus/usb Read/Write gemountet, usblp nicht geladen. " "In Unraid pruefen: Privileged an, /dev/bus/usb Read/Write gemountet, usblp nicht geladen. "
f"Details: {details}" f"Details: {details}"
) )
if "timeout while waiting for status response" in details:
return (
"Drucker wurde gefunden, antwortet aber nicht auf die Statusabfrage. "
"PT-P700 kurz ausschalten oder USB abziehen, usblp auf dem Host erneut pruefen/entladen "
"und danach den Container neu starten. "
f"Details: {details}"
)
if result.returncode == 124 or "Druckprozess wurde nach Timeout beendet" in details:
return (
"Druckprozess hing und wurde beendet. "
"Bitte auf dem Host pruefen, ob kein ptouch-print mehr das USB-Geraet blockiert, "
"danach Drucker kurz neu verbinden und erneut versuchen. "
f"Details: {details}"
)
return f"Druck fehlgeschlagen: {details}" return f"Druck fehlgeschlagen: {details}"
@@ -326,10 +440,15 @@ def api_print():
design, copies = design_from_request() design, copies = design_from_request()
image_path = render_design(design) image_path = render_design(design)
if PRINT_ENABLED: if PRINT_ENABLED:
results = print_label(image_path, copies) if not PRINT_LOCK.acquire(blocking=False):
failed = next((result for result in results if result.returncode != 0), None) raise RuntimeError("Es laeuft bereits ein Druckjob. Bitte warten oder haengenden ptouch-print-Prozess beenden.")
if failed: try:
raise RuntimeError(format_print_error(failed)) results = print_label(image_path, copies, design)
failed = next((result for result in results if result.returncode != 0), None)
if failed:
raise RuntimeError(format_print_error(failed))
finally:
PRINT_LOCK.release()
message = "Etikett wurde an den Drucker gesendet." message = "Etikett wurde an den Drucker gesendet."
else: else:
message = f"Testmodus: Etikett erstellt, nicht gedruckt: {image_path.name}" message = f"Testmodus: Etikett erstellt, nicht gedruckt: {image_path.name}"
+44 -7
View File
@@ -1,4 +1,12 @@
const MM_TO_SCREEN = 6; const MM_TO_SCREEN = 6;
const PRINTABLE_TAPE_PX = {
3.5: 24,
6: 32,
9: 52,
12: 76,
18: 120,
24: 128,
};
const STORAGE_KEY = "ptouch-web-layout"; const STORAGE_KEY = "ptouch-web-layout";
const state = { const state = {
@@ -10,6 +18,8 @@ const state = {
lengthMm: 70, lengthMm: 70,
marginMm: 2, marginMm: 2,
orientation: "landscape", orientation: "landscape",
cutMode: "auto",
halfcut: "on",
objects: [], objects: [],
}, },
}; };
@@ -22,6 +32,8 @@ const controls = {
tapeWidth: document.getElementById("tapeWidth"), tapeWidth: document.getElementById("tapeWidth"),
lengthMm: document.getElementById("lengthMm"), lengthMm: document.getElementById("lengthMm"),
marginMm: document.getElementById("marginMm"), marginMm: document.getElementById("marginMm"),
cutMode: document.getElementById("cutMode"),
halfcut: document.getElementById("halfcut"),
copies: document.getElementById("copies"), copies: document.getElementById("copies"),
zoomLabel: document.getElementById("zoomLabel"), zoomLabel: document.getElementById("zoomLabel"),
previewImage: document.getElementById("previewImage"), previewImage: document.getElementById("previewImage"),
@@ -64,6 +76,11 @@ function pxToMm(v) {
return v / scale(); return v / scale();
} }
function printablePxPerMm() {
const tape = Number(state.design.tapeWidthMm);
return (PRINTABLE_TAPE_PX[tape] || (tape * 180) / 25.4) / tape;
}
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;
} }
@@ -77,6 +94,8 @@ function syncDesignControls() {
state.design.tapeWidthMm = Number(controls.tapeWidth.value); state.design.tapeWidthMm = Number(controls.tapeWidth.value);
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.halfcut = controls.halfcut.value;
state.design.orientation = document.querySelector("input[name='orientation']:checked").value; state.design.orientation = document.querySelector("input[name='orientation']:checked").value;
} }
@@ -84,6 +103,8 @@ 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.marginMm.value = state.design.marginMm; controls.marginMm.value = state.design.marginMm;
controls.cutMode.value = state.design.cutMode || "auto";
controls.halfcut.value = state.design.halfcut || "on";
document.querySelector(`input[name='orientation'][value='${state.design.orientation}']`).checked = true; document.querySelector(`input[name='orientation'][value='${state.design.orientation}']`).checked = true;
} }
@@ -114,21 +135,25 @@ function drawRulers() {
function drawTextObject(obj) { function drawTextObject(obj) {
ctx.save(); ctx.save();
ctx.fillStyle = "#000";
ctx.font = `${obj.bold ? "700 " : ""}${mmToPx(obj.fontSizeMm || 5)}px system-ui, sans-serif`;
ctx.textBaseline = "middle";
ctx.textAlign = obj.align || "left";
const x = mmToPx(obj.x); const x = mmToPx(obj.x);
const y = mmToPx(obj.y); const y = mmToPx(obj.y);
const w = mmToPx(obj.w); const w = mmToPx(obj.w);
const h = mmToPx(obj.h); const h = mmToPx(obj.h);
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
ctx.fillStyle = "#000";
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";
ctx.textAlign = obj.align || "left";
const lines = String(obj.text || "").split("\n"); const lines = String(obj.text || "").split("\n");
const lineHeight = mmToPx((obj.fontSizeMm || 5) * 1.15); const lineHeight = fontPx * 1.15;
const total = lineHeight * lines.length; const total = lineHeight * lines.length;
let cy = y + h / 2 - total / 2 + lineHeight / 2; let cy = y + h / 2 - total / 2 + lineHeight / 2;
const tx = obj.align === "center" ? x + w / 2 : obj.align === "right" ? x + w : x; const tx = obj.align === "center" ? x + w / 2 : obj.align === "right" ? x + w : x;
for (const line of lines) { for (const line of lines) {
ctx.fillText(line, tx, cy, w); ctx.fillText(line, tx, cy);
cy += lineHeight; cy += lineHeight;
} }
ctx.restore(); ctx.restore();
@@ -212,6 +237,18 @@ function draw() {
ctx.setLineDash([]); ctx.setLineDash([]);
for (const obj of state.design.objects) drawObject(obj); for (const obj of state.design.objects) drawObject(obj);
if (state.design.cutMode === "cutmark" || state.design.cutMode === "cutmark-chain") {
ctx.strokeStyle = "#111";
ctx.setLineDash([4, 4]);
const x = canvas.width - mmToPx(1.5);
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
ctx.setLineDash([]);
}
drawRulers(); drawRulers();
controls.zoomLabel.textContent = `${Math.round(state.zoom * 100)}%`; controls.zoomLabel.textContent = `${Math.round(state.zoom * 100)}%`;
updateProps(); updateProps();
@@ -376,7 +413,7 @@ window.addEventListener("keydown", (event) => {
} }
}); });
["tapeWidth", "lengthMm", "marginMm"].forEach((id) => document.getElementById(id).addEventListener("input", draw)); ["tapeWidth", "lengthMm", "marginMm", "cutMode", "halfcut"].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);
+4
View File
@@ -6,6 +6,8 @@ const fields = {
font: document.getElementById("mobileFont"), font: document.getElementById("mobileFont"),
copies: document.getElementById("mobileCopies"), copies: document.getElementById("mobileCopies"),
align: document.getElementById("mobileAlign"), align: document.getElementById("mobileAlign"),
cutMode: document.getElementById("mobileCutMode"),
halfcut: document.getElementById("mobileHalfcut"),
bold: document.getElementById("mobileBold"), bold: document.getElementById("mobileBold"),
frame: document.getElementById("mobileFrame"), frame: document.getElementById("mobileFrame"),
preview: document.getElementById("mobilePreview"), preview: document.getElementById("mobilePreview"),
@@ -53,6 +55,8 @@ function mobileDesign() {
lengthMm: length, lengthMm: length,
marginMm: margin, marginMm: margin,
orientation: "landscape", orientation: "landscape",
cutMode: fields.cutMode.value,
halfcut: fields.halfcut.value,
objects, objects,
}; };
} }
+4 -2
View File
@@ -269,14 +269,16 @@ textarea {
.preview-box { .preview-box {
min-height: 120px; min-height: 120px;
display: grid; display: grid;
place-items: center; align-items: center;
justify-items: start;
border: 1px dashed #555; border: 1px dashed #555;
background: #222; background: #222;
padding: 12px; padding: 12px;
overflow: auto;
} }
.preview-box img { .preview-box img {
max-width: 100%; max-width: none;
image-rendering: crisp-edges; image-rendering: crisp-edges;
} }
+16
View File
@@ -68,6 +68,22 @@
Kopien Kopien
<input id="copies" type="number" min="1" max="20" step="1" value="1"> <input id="copies" type="number" min="1" max="20" step="1" value="1">
</label> </label>
<label>
Schnitt
<select id="cutMode">
<option value="auto">Normal</option>
<option value="chain">Kettendruck / kein Endschnitt</option>
<option value="cutmark">Schnittmarke drucken</option>
<option value="cutmark-chain">Kettendruck + Schnittmarke</option>
</select>
</label>
<label>
Halbschnitt
<select id="halfcut">
<option value="on">Ein</option>
<option value="off">Aus</option>
</select>
</label>
</section> </section>
<section> <section>
+17
View File
@@ -67,6 +67,23 @@
</select> </select>
</label> </label>
<label>
Schnitt
<select id="mobileCutMode">
<option value="auto">Normal</option>
<option value="chain">Kettendruck</option>
<option value="cutmark">Schnittmarke</option>
<option value="cutmark-chain">Kettendruck + Marke</option>
</select>
</label>
<label>
Halbschnitt
<select id="mobileHalfcut">
<option value="on">Ein</option>
<option value="off">Aus</option>
</select>
</label>
<div class="check-row"> <div class="check-row">
<label><input id="mobileBold" type="checkbox" checked> Fett</label> <label><input id="mobileBold" type="checkbox" checked> Fett</label>
<label><input id="mobileFrame" type="checkbox"> Rahmen</label> <label><input id="mobileFrame" type="checkbox"> Rahmen</label>
+52
View File
@@ -0,0 +1,52 @@
--- a/src/ptouch-print.c
+++ b/src/ptouch-print.c
@@ -58,6 +58,7 @@
int fontsize = 0;
bool debug = false;
bool chain = false;
+int precut = 1;
int forced_tape_width = 0;
/* --------------------------------------------------------------------
@@ -123,8 +124,8 @@
}
}
if ((ptdev->devinfo->flags & FLAG_HAS_PRECUT) == FLAG_HAS_PRECUT) {
- ptouch_send_precut_cmd(ptdev, 1);
- if (debug) {
+ if (precut) ptouch_send_precut_cmd(ptdev, 1);
+ if (debug && precut) {
printf(_("send precut command\n"));
}
}
@@ -439,6 +440,8 @@
printf("\t--cutmark\t\tPrint a mark where the tape should be cut\n");
printf("\t--pad <n>\t\tAdd n pixels padding (blank tape)\n");
printf("\t--chain\t\t\tSkip final feed of label and any automatic cut\n");
+ printf("\t--halfcut\t\tEnable half cut / precut when supported\n");
+ printf("\t--no-halfcut\t\tDisable half cut / precut when supported\n");
printf("other commands:\n");
printf("\t--version\t\tshow version info (required for bug report)\n");
printf("\t--info\t\t\tshow info about detected tape\n");
@@ -484,6 +487,10 @@
continue; /* not done here */
} else if (strcmp(&argv[i][1], "-chain") == 0) {
chain=true;
+ } else if (strcmp(&argv[i][1], "-halfcut") == 0) {
+ precut=1;
+ } else if (strcmp(&argv[i][1], "-no-halfcut") == 0) {
+ precut=0;
} else if (strcmp(&argv[i][1], "-debug") == 0) {
debug=true;
} else if (strcmp(&argv[i][1], "-info") == 0) {
@@ -634,6 +641,10 @@
im = NULL;
} else if (strcmp(&argv[i][1], "-chain") == 0) {
chain = true;
+ } else if (strcmp(&argv[i][1], "-halfcut") == 0) {
+ precut = 1;
+ } else if (strcmp(&argv[i][1], "-no-halfcut") == 0) {
+ precut = 0;
} else if (strcmp(&argv[i][1], "-debug") == 0) {
debug = true;
} else if (strcmp(&argv[i][1], "-copies") == 0) {
+4 -1
View File
@@ -56,9 +56,12 @@ if ! python3 -c "import qrcode, barcode" >/dev/null 2>&1; then
python3 -m pip install --root-user-action=ignore --no-cache-dir qrcode python-barcode python3 -m pip install --root-user-action=ignore --no-cache-dir qrcode python-barcode
fi fi
if ! command -v ptouch-print >/dev/null 2>&1; then if ! command -v ptouch-print >/dev/null 2>&1 || ! ptouch-print --help 2>&1 | grep -q -- "--no-halfcut"; then
rm -rf "$PTOUCH_SRC_DIR" rm -rf "$PTOUCH_SRC_DIR"
git clone --depth 1 --branch farix-main https://github.com/farixembedded/ptouch-print.git "$PTOUCH_SRC_DIR" git clone --depth 1 --branch farix-main https://github.com/farixembedded/ptouch-print.git "$PTOUCH_SRC_DIR"
if [ -f "$APP_DIR/patches/ptouch-print-cut-options.patch" ]; then
patch -d "$PTOUCH_SRC_DIR" -p1 < "$APP_DIR/patches/ptouch-print-cut-options.patch"
fi
cmake -S "$PTOUCH_SRC_DIR" -B "$PTOUCH_SRC_DIR/build" -DCMAKE_BUILD_TYPE=Release cmake -S "$PTOUCH_SRC_DIR" -B "$PTOUCH_SRC_DIR/build" -DCMAKE_BUILD_TYPE=Release
cmake --build "$PTOUCH_SRC_DIR/build" cmake --build "$PTOUCH_SRC_DIR/build"
install -m 0755 "$PTOUCH_SRC_DIR/build/ptouch-print" /usr/local/bin/ptouch-print install -m 0755 "$PTOUCH_SRC_DIR/build/ptouch-print" /usr/local/bin/ptouch-print