Add mobile mode and print cut controls
This commit is contained in:
+3
-1
@@ -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 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
|
||||
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 \
|
||||
&& install -m 0755 build/ptouch-print /usr/local/bin/ptouch-print
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Die Weboberflaeche bildet die wichtigsten P-touch-Editor-Funktionen nach:
|
||||
- Layout speichern/laden im Browser
|
||||
- serverseitige Vorschau und Druck ueber `ptouch-print`
|
||||
- vereinfachte Mobile-Webansicht unter `/mobile`
|
||||
- Schnittmodi: normal, Halbschnitt ein/aus, Kettendruck, Schnittmarke, Kettendruck mit Schnittmarke
|
||||
|
||||
## Wichtig fuer den PT-P700
|
||||
|
||||
@@ -103,7 +104,9 @@ Umgebungsvariablen:
|
||||
- `LABEL_DPI=180` passt zum PT-P700.
|
||||
- `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
|
||||
|
||||
|
||||
+152
-33
@@ -2,10 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import signal
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import threading
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
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_ENABLED = os.environ.get("PRINT_ENABLED", "1").lower() in {"1", "true", "yes", "on"}
|
||||
DPI = int(os.environ.get("LABEL_DPI", "180"))
|
||||
PTOUCH_SUPPORTS_HALFCUT: bool | None = None
|
||||
|
||||
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.secret_key = os.environ.get("SECRET_KEY", "change-me")
|
||||
PRINT_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def mm_to_px(mm: float) -> int:
|
||||
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:
|
||||
return max(low, min(high, value))
|
||||
|
||||
@@ -67,11 +92,11 @@ def new_label_image(width_px: int, height_px: int) -> Image.Image:
|
||||
return image
|
||||
|
||||
|
||||
def object_box(obj: dict[str, Any]) -> tuple[int, int, int, int]:
|
||||
x = mm_to_px(float(obj.get("x", 0)))
|
||||
y = mm_to_px(float(obj.get("y", 0)))
|
||||
w = mm_to_px(float(obj.get("w", 10)))
|
||||
h = mm_to_px(float(obj.get("h", 5)))
|
||||
def object_box(obj: dict[str, Any], scale: float) -> tuple[int, int, int, int]:
|
||||
x = scaled_mm_to_px(float(obj.get("x", 0)), scale)
|
||||
y = scaled_mm_to_px(float(obj.get("y", 0)), scale)
|
||||
w = scaled_mm_to_px(float(obj.get("w", 10)), scale)
|
||||
h = scaled_mm_to_px(float(obj.get("h", 5)), scale)
|
||||
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
|
||||
|
||||
|
||||
def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
|
||||
x, y, w, h = object_box(obj)
|
||||
def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
|
||||
x, y, w, h = object_box(obj, scale)
|
||||
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)))
|
||||
align = obj.get("align", "left")
|
||||
valign = obj.get("valign", "middle")
|
||||
@@ -122,9 +147,9 @@ def draw_text(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
|
||||
cy += line_h + gap
|
||||
|
||||
|
||||
def draw_shape(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
|
||||
x, y, w, h = object_box(obj)
|
||||
line = max(1, mm_to_px(float(obj.get("lineMm", 0.25))))
|
||||
def draw_shape(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
|
||||
x, y, w, h = object_box(obj, scale)
|
||||
line = max(1, scaled_mm_to_px(float(obj.get("lineMm", 0.25)), scale))
|
||||
shape = obj.get("shape", "rect")
|
||||
fill = 1 if obj.get("fill", False) else None
|
||||
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)))
|
||||
|
||||
|
||||
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:
|
||||
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.add_data(str(obj.get("text", "https://example.local")))
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
raise RuntimeError("python-barcode ist nicht installiert.")
|
||||
x, y, w, h = object_box(obj)
|
||||
x, y, w, h = object_box(obj, scale)
|
||||
writer = ImageWriter()
|
||||
code = barcode.get("code128", str(obj.get("text", "1234567890")), writer=writer)
|
||||
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)
|
||||
|
||||
|
||||
def draw_table(draw: ImageDraw.ImageDraw, obj: dict[str, Any]) -> None:
|
||||
x, y, w, h = object_box(obj)
|
||||
def draw_table(draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None:
|
||||
x, y, w, h = object_box(obj, scale)
|
||||
rows = max(1, int(obj.get("rows", 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)
|
||||
for col in range(1, 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)
|
||||
|
||||
|
||||
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:
|
||||
tape_width = float(design.get("tapeWidthMm", 24))
|
||||
if tape_width not in TAPE_WIDTHS_MM:
|
||||
@@ -199,7 +234,14 @@ def render_design(design: dict[str, Any]) -> Path:
|
||||
else:
|
||||
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)
|
||||
|
||||
for obj in design.get("objects", []):
|
||||
@@ -207,35 +249,93 @@ def render_design(design: dict[str, Any]) -> Path:
|
||||
continue
|
||||
obj_type = obj.get("type")
|
||||
if obj_type == "text" or obj_type == "symbol":
|
||||
draw_text(draw, obj)
|
||||
draw_text(draw, obj, scale)
|
||||
elif obj_type == "shape":
|
||||
draw_shape(draw, obj)
|
||||
draw_shape(draw, obj, scale)
|
||||
elif obj_type == "line":
|
||||
draw_shape(draw, {**obj, "shape": "line"})
|
||||
draw_shape(draw, {**obj, "shape": "line"}, scale)
|
||||
elif obj_type == "table":
|
||||
draw_table(draw, obj)
|
||||
draw_table(draw, obj, scale)
|
||||
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)
|
||||
elif obj_type == "qr":
|
||||
draw_qr(image, obj)
|
||||
draw_qr(image, obj, scale)
|
||||
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"
|
||||
image.save(output)
|
||||
return output
|
||||
|
||||
|
||||
def run_print_command(image_path: Path, copies: int) -> subprocess.CompletedProcess[str]:
|
||||
command = PRINT_COMMAND.format(image=shlex.quote(str(image_path)), copies=copies)
|
||||
return subprocess.run(shlex.split(command), capture_output=True, check=False, text=True, timeout=90)
|
||||
def ptouch_supports_halfcut() -> bool:
|
||||
global PTOUCH_SUPPORTS_HALFCUT
|
||||
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:
|
||||
return [run_print_command(image_path, copies)]
|
||||
return [run_print_command(image_path, 1) for _ in range(copies)]
|
||||
return [run_print_command(image_path, copies, design)]
|
||||
return [run_print_command(image_path, 1, design) for _ in range(copies)]
|
||||
|
||||
|
||||
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. "
|
||||
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}"
|
||||
|
||||
|
||||
@@ -326,10 +440,15 @@ def api_print():
|
||||
design, copies = design_from_request()
|
||||
image_path = render_design(design)
|
||||
if PRINT_ENABLED:
|
||||
results = print_label(image_path, copies)
|
||||
if not PRINT_LOCK.acquire(blocking=False):
|
||||
raise RuntimeError("Es laeuft bereits ein Druckjob. Bitte warten oder haengenden ptouch-print-Prozess beenden.")
|
||||
try:
|
||||
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."
|
||||
else:
|
||||
message = f"Testmodus: Etikett erstellt, nicht gedruckt: {image_path.name}"
|
||||
|
||||
+44
-7
@@ -1,4 +1,12 @@
|
||||
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 state = {
|
||||
@@ -10,6 +18,8 @@ const state = {
|
||||
lengthMm: 70,
|
||||
marginMm: 2,
|
||||
orientation: "landscape",
|
||||
cutMode: "auto",
|
||||
halfcut: "on",
|
||||
objects: [],
|
||||
},
|
||||
};
|
||||
@@ -22,6 +32,8 @@ const controls = {
|
||||
tapeWidth: document.getElementById("tapeWidth"),
|
||||
lengthMm: document.getElementById("lengthMm"),
|
||||
marginMm: document.getElementById("marginMm"),
|
||||
cutMode: document.getElementById("cutMode"),
|
||||
halfcut: document.getElementById("halfcut"),
|
||||
copies: document.getElementById("copies"),
|
||||
zoomLabel: document.getElementById("zoomLabel"),
|
||||
previewImage: document.getElementById("previewImage"),
|
||||
@@ -64,6 +76,11 @@ function pxToMm(v) {
|
||||
return v / scale();
|
||||
}
|
||||
|
||||
function printablePxPerMm() {
|
||||
const tape = Number(state.design.tapeWidthMm);
|
||||
return (PRINTABLE_TAPE_PX[tape] || (tape * 180) / 25.4) / tape;
|
||||
}
|
||||
|
||||
function selected() {
|
||||
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.lengthMm = Number(controls.lengthMm.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;
|
||||
}
|
||||
|
||||
@@ -84,6 +103,8 @@ function applyDesignControls() {
|
||||
controls.tapeWidth.value = state.design.tapeWidthMm;
|
||||
controls.lengthMm.value = state.design.lengthMm;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -114,21 +135,25 @@ function drawRulers() {
|
||||
|
||||
function drawTextObject(obj) {
|
||||
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 y = mmToPx(obj.y);
|
||||
const w = mmToPx(obj.w);
|
||||
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 lineHeight = mmToPx((obj.fontSizeMm || 5) * 1.15);
|
||||
const lineHeight = fontPx * 1.15;
|
||||
const total = lineHeight * lines.length;
|
||||
let cy = y + h / 2 - total / 2 + lineHeight / 2;
|
||||
const tx = obj.align === "center" ? x + w / 2 : obj.align === "right" ? x + w : x;
|
||||
for (const line of lines) {
|
||||
ctx.fillText(line, tx, cy, w);
|
||||
ctx.fillText(line, tx, cy);
|
||||
cy += lineHeight;
|
||||
}
|
||||
ctx.restore();
|
||||
@@ -212,6 +237,18 @@ function draw() {
|
||||
ctx.setLineDash([]);
|
||||
|
||||
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();
|
||||
controls.zoomLabel.textContent = `${Math.round(state.zoom * 100)}%`;
|
||||
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));
|
||||
Object.values(controls).forEach((control) => {
|
||||
if (control && control.id && control.id.startsWith("prop")) control.addEventListener("input", applyProps);
|
||||
|
||||
@@ -6,6 +6,8 @@ const fields = {
|
||||
font: document.getElementById("mobileFont"),
|
||||
copies: document.getElementById("mobileCopies"),
|
||||
align: document.getElementById("mobileAlign"),
|
||||
cutMode: document.getElementById("mobileCutMode"),
|
||||
halfcut: document.getElementById("mobileHalfcut"),
|
||||
bold: document.getElementById("mobileBold"),
|
||||
frame: document.getElementById("mobileFrame"),
|
||||
preview: document.getElementById("mobilePreview"),
|
||||
@@ -53,6 +55,8 @@ function mobileDesign() {
|
||||
lengthMm: length,
|
||||
marginMm: margin,
|
||||
orientation: "landscape",
|
||||
cutMode: fields.cutMode.value,
|
||||
halfcut: fields.halfcut.value,
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,14 +269,16 @@ textarea {
|
||||
.preview-box {
|
||||
min-height: 120px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
border: 1px dashed #555;
|
||||
background: #222;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-box img {
|
||||
max-width: 100%;
|
||||
max-width: none;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,22 @@
|
||||
Kopien
|
||||
<input id="copies" type="number" min="1" max="20" step="1" value="1">
|
||||
</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>
|
||||
|
||||
@@ -67,6 +67,23 @@
|
||||
</select>
|
||||
</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">
|
||||
<label><input id="mobileBold" type="checkbox" checked> Fett</label>
|
||||
<label><input id="mobileFrame" type="checkbox"> Rahmen</label>
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
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"
|
||||
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 --build "$PTOUCH_SRC_DIR/build"
|
||||
install -m 0755 "$PTOUCH_SRC_DIR/build/ptouch-print" /usr/local/bin/ptouch-print
|
||||
|
||||
Reference in New Issue
Block a user