Initial release

This commit is contained in:
MrLetsPlay4Fun 2026-02-03 12:55:24 +01:00
commit 1b9bafdc4a
7 changed files with 740 additions and 0 deletions

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.egg-info/
dist/
build/
.eggs/
# Virtualenv
.venv/
venv/
ENV/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 MrLetsPlay4Fun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

161
README.md Normal file
View file

@ -0,0 +1,161 @@
# paperless-ngx ASN Label Generator (GUI) Avery L4731 / L4731REV
GUI-Tool (Tkinter), das ASN-Labels als PDF erzeugt QR oder Code128 inkl. Kalibrierung (Offset & Pitch-Drift) für saubere Ausdrucke auf echten Etikettenbögen.
> Hinweis: Inoffizielles Tool, nicht affiliated mit paperless-ngx.
---
## Voraussetzungen
- **Python 3.10+**
- **Git** (nur nötig, wenn du direkt von GitHub installieren willst)
- Auf **Linux** ggf. zusätzlich: **Tkinter** (siehe unten)
---
## Installation & Start
### Windows (empfohlen)
#### 1) Python installieren
- Lade Python von der offiziellen Seite.
- **Wichtig:** Beim Installer unbedingt **„Add Python to PATH“** aktivieren.
Prüfen:
```bash
python --version
pip --version
```
#### 2) Installation direkt von GitHub
```bash
pip install git+https://github.com/DEINNAME/paperless-asn-label-generator
paperless-asn-labels
```
> Wenn `pip` meldet, dass `git` fehlt: Git für Windows installieren, dann nochmal versuchen.
---
### macOS
#### 1) Python prüfen/ installieren
Prüfen:
```bash
python3 --version
pip3 --version
```
Falls Python fehlt oder zu alt ist, am einfachsten über Homebrew:
```bash
brew install python
```
#### 2) Installation direkt von GitHub
```bash
pip3 install git+https://github.com/DEINNAME/paperless-asn-label-generator
paperless-asn-labels
```
> Falls `paperless-asn-labels` nicht gefunden wird, starte einmal neu das Terminal oder nutze:
```bash
python3 -m paperless_asn_label_generator
```
---
### Linux
#### 1) Python + pip prüfen
```bash
python3 --version
pip3 --version
```
Falls nötig (Debian/Ubuntu):
```bash
sudo apt-get update
sudo apt-get install -y python3 python3-pip
```
#### 2) Tkinter installieren (wichtig für GUI)
Debian/Ubuntu:
```bash
sudo apt-get install -y python3-tk
```
Fedora:
```bash
sudo dnf install -y python3-tkinter
```
Arch:
```bash
sudo pacman -S tk
```
#### 3) Installation direkt von GitHub
```bash
pip3 install git+https://github.com/DEINNAME/paperless-asn-label-generator
paperless-asn-labels
```
---
## Nutzung
- Start-Nummer wählen (z.B. 1 oder 190)
- Prefix setzen (muss zu paperless-ngx passen)
- Führende Nullen einstellen (z.B. 7 → `ASN0000001`)
- Menge als **Labels** oder **A4-Blätter**
- Code-Typ: **QR** oder **CODE128**
- Optional: Rahmen aktivieren (hilft beim Kalibrieren)
- „PDF erzeugen…“ → Datei speichern
---
## Prefix in paperless-ngx
Der Prefix muss zu deiner paperless-ngx Einstellung passen:
- `PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX`
Beispiel: Wenn du in paperless-ngx `ASN` nutzt, dann hier auch `ASN`.
---
## Druck-Hinweis (sehr wichtig)
Bitte im PDF-Viewer/Printer **100%** drucken:
- **kein** „An Seite anpassen“
- **kein** „Skalieren“
- **kein** „Fit to Page“
Wenn die Position nicht exakt passt:
- Rahmen aktivieren
- Offset X/Y und Pitch-Δ anpassen
---
## Entwicklung (Repo klonen & lokal starten)
```bash
git clone https://github.com/DEINNAME/paperless-asn-label-generator
cd paperless-asn-label-generator
# Virtuelle Umgebung
python -m venv .venv
# Windows:
# .venv\Scripts\activate
# macOS/Linux:
# source .venv/bin/activate
pip install -e .
paperless-asn-labels
```
---
## Lizenz
MIT (siehe LICENSE)

34
pyproject.toml Normal file
View file

@ -0,0 +1,34 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "paperless-asn-label-generator"
version = "0.1.0"
description = "GUI tool to generate ASN barcode/QR labels for paperless-ngx (A4/Avery L4731) with calibration."
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [{ name = "MrLetsPlay4Fun" }]
keywords = ["paperless-ngx", "labels", "asn", "qr", "barcode", "avery", "reportlab", "tkinter"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
dependencies = [
"reportlab>=4.0.0",
"qrcode[pil]>=7.4.2",
"Pillow>=10.0.0"
]
[project.scripts]
paperless-asn-labels = "paperless_asn_label_generator.app:main"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]

View file

@ -0,0 +1,2 @@
__all__ = ["__version__"]
__version__ = "0.1.0"

View file

@ -0,0 +1,4 @@
from .app import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,495 @@
#!/usr/bin/env python3
"""
paperless-ngx ASN Label Generator (GUI) + Calibration
This version adds calibration controls to fix typical "top row too low / bottom row too high"
and left/right drift issues when printing on real label sheets (e.g. Avery Zweckform L4731/L4731REV).
New controls:
- Offset X/Y (mm): shifts the whole grid
- Spalten-Pitch Δ (mm): changes distance between columns (fixes left/right drift)
- Zeilen-Pitch Δ (mm): changes distance between rows (fixes top/bottom drift)
Tip: In your PDF viewer / printer settings, print at 100% (no "Fit to page"/"Shrink/Expand").
"""
from __future__ import annotations
import math
import os
import sys
from dataclasses import dataclass
from typing import Literal, Tuple
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from reportlab.pdfgen import canvas as rl_canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm, cm
from reportlab.lib.utils import ImageReader
from reportlab.graphics.barcode import code128
import qrcode
from PIL import Image, ImageTk
BarcodeKind = Literal["QR", "CODE128"]
@dataclass(frozen=True)
class SheetLayout:
name: str
cols: int
rows: int
label_w: float # points
label_h: float # points
gap_x: float # points (base gap)
gap_y: float # points (base gap)
margin_left: float # points
margin_top: float # points
@property
def labels_per_page(self) -> int:
return self.cols * self.rows
@property
def pitch_x(self) -> float:
return self.label_w + self.gap_x
@property
def pitch_y(self) -> float:
return self.label_h + self.gap_y
# NOTE: Many shops/templates call this "L4731" (25.4 x 10mm, 7x27 = 189 labels).
# If your sheet is slightly different, use calibration below.
AVERY_L4731 = SheetLayout(
name="Avery Zweckform L4731 / L4731REV (7x27, 25.4mm x 10mm)",
cols=7,
rows=27,
label_w=25.4 * mm,
label_h=10.0 * mm,
gap_x=0.25 * cm, # base guess (tweak with Δ if needed)
gap_y=0.0 * cm, # base guess (tweak with Δ if needed)
margin_left=0.85 * cm,
margin_top=1.35 * cm,
)
def make_asn_text(prefix: str, number: int, leading_zeros: int) -> str:
return f"{prefix}{number:0{leading_zeros}d}"
def make_qr_image(data: str, target_px: int) -> Image.Image:
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=10,
border=2,
)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
return img.resize((target_px, target_px), Image.Resampling.LANCZOS)
def draw_label(
c: rl_canvas.Canvas,
x: float,
y: float,
layout: SheetLayout,
text: str,
kind: BarcodeKind,
draw_border: bool,
) -> None:
"""Draw one label in the rectangle [x, y, label_w, label_h] (origin bottom-left)."""
if draw_border:
c.rect(x, y, layout.label_w, layout.label_h, stroke=1, fill=0)
pad = 1.0 * mm
code_size = layout.label_h * 0.90
code_x = x + pad
code_y = y + (layout.label_h - code_size) / 2.0
if kind == "QR":
dpi = 600
target_px = max(80, int(code_size * dpi / 72.0))
img = make_qr_image(text, target_px)
c.drawImage(ImageReader(img), code_x, code_y, width=code_size, height=code_size, preserveAspectRatio=True, mask=None)
else:
b = code128.Code128(text, barHeight=code_size * 0.85, humanReadable=False)
desired_w = code_size
scale = min(1.0, desired_w / float(b.width))
c.saveState()
c.translate(code_x, code_y + (code_size - b.barHeight) / 2.0)
c.scale(scale, 1.0)
b.drawOn(c, 0, 0)
c.restoreState()
text_x = code_x + code_size + pad
font_size = max(5.5, min(8.0, (layout.label_h / mm) * 0.55))
c.setFont("Helvetica", font_size)
c.drawString(text_x, y + (layout.label_h - font_size) / 2.0, text)
def generate_pdf(
output_path: str,
start_number: int,
count: int,
prefix: str,
leading_zeros: int,
kind: BarcodeKind,
layout: SheetLayout = AVERY_L4731,
draw_border: bool = False,
# NEW calibration:
offset_x_mm: float = 0.0, # + moves right
offset_y_mm: float = 0.0, # + moves up
pitch_dx_mm: float = 0.0, # added to pitch_x (distance between columns)
pitch_dy_mm: float = 0.0, # added to pitch_y (distance between rows)
) -> Tuple[int, int]:
"""
Returns: (pages_generated, next_number)
"""
if count <= 0:
raise ValueError("count must be > 0")
if start_number <= 0:
raise ValueError("start_number must be > 0")
if leading_zeros < 0:
raise ValueError("leading_zeros must be >= 0")
if not prefix:
raise ValueError("prefix must not be empty")
_page_w, page_h = A4
labels_per_page = layout.labels_per_page
pages = math.ceil(count / labels_per_page)
# Effective pitch + offsets (points)
pitch_x = layout.pitch_x + (pitch_dx_mm * mm)
pitch_y = layout.pitch_y + (pitch_dy_mm * mm)
off_x = offset_x_mm * mm
off_y = offset_y_mm * mm
c = rl_canvas.Canvas(output_path, pagesize=A4)
current = start_number
remaining = count
for _ in range(pages):
n_this_page = min(remaining, labels_per_page)
for i in range(n_this_page):
r = i // layout.cols
col = i % layout.cols
x = layout.margin_left + col * pitch_x + off_x
y = page_h - layout.margin_top - layout.label_h - r * pitch_y + off_y
text = make_asn_text(prefix, current, leading_zeros)
draw_label(c, x, y, layout, text, kind, draw_border)
current += 1
c.showPage()
remaining -= n_this_page
c.save()
return pages, current
class App(ttk.Frame):
def __init__(self, master: tk.Tk) -> None:
super().__init__(master, padding=12)
self.master = master
self.var_start = tk.StringVar(value="1")
# Quantity mode
self.var_mode = tk.StringVar(value="labels") # "labels" or "pages"
self.var_count = tk.StringVar(value=str(AVERY_L4731.labels_per_page))
self.var_pages = tk.StringVar(value="1")
self.var_prefix = tk.StringVar(value="ASN")
self.var_zeros = tk.StringVar(value="7")
self.var_kind = tk.StringVar(value="QR")
self.var_border = tk.BooleanVar(value=False)
# NEW calibration vars (mm)
self.var_off_x = tk.StringVar(value="-4.5")
self.var_off_y = tk.StringVar(value="7.0")
self.var_pitch_dx = tk.StringVar(value="1.2")
self.var_pitch_dy = tk.StringVar(value="0.42")
self._preview_imgtk: ImageTk.PhotoImage | None = None
self._count_entry: ttk.Entry | None = None
self._pages_entry: ttk.Entry | None = None
self._build_ui()
self._apply_mode()
self._update_preview()
def _build_ui(self) -> None:
self.master.title("paperless-ngx ASN Label Generator (A4 / Avery L4731) mit Kalibrierung")
self.grid(sticky="nsew")
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
frm = ttk.LabelFrame(self, text="Einstellungen")
frm.grid(row=0, column=0, sticky="nsew")
for i in range(4):
frm.columnconfigure(i, weight=1)
def add_row(label: str, var: tk.StringVar, row: int, hint: str = "", store: str | None = None) -> None:
ttk.Label(frm, text=label).grid(row=row, column=0, sticky="w", padx=(10, 6), pady=6)
ent = ttk.Entry(frm, textvariable=var, width=14)
ent.grid(row=row, column=1, sticky="we", padx=(0, 10), pady=6)
if hint:
ttk.Label(frm, text=hint, foreground="#555").grid(row=row, column=2, columnspan=2, sticky="w", padx=(0, 10), pady=6)
ent.bind("<KeyRelease>", lambda _e: self._update_preview())
if store == "count":
self._count_entry = ent
elif store == "pages":
self._pages_entry = ent
add_row("Start-Nummer", self.var_start, 0, "z.B. 1 oder 190")
ttk.Label(frm, text="Menge als").grid(row=1, column=0, sticky="w", padx=(10, 6), pady=6)
mode_box = ttk.Frame(frm)
mode_box.grid(row=1, column=1, sticky="w", padx=(0, 10), pady=6)
ttk.Radiobutton(mode_box, text="Labels", value="labels", variable=self.var_mode, command=self._apply_mode).pack(side="left", padx=(0, 10))
ttk.Radiobutton(mode_box, text="A4 Blätter", value="pages", variable=self.var_mode, command=self._apply_mode).pack(side="left")
add_row("Anzahl Labels", self.var_count, 2, f"{AVERY_L4731.labels_per_page} Labels pro A4-Seite", store="count")
add_row("Anzahl A4 Blätter", self.var_pages, 3, "z.B. 3 → erzeugt 3 Seiten", store="pages")
add_row("Prefix", self.var_prefix, 4, "Muss zu PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX passen")
add_row("Führende Nullen", self.var_zeros, 5, "z.B. 5 → ASN00001")
ttk.Label(frm, text="Code-Typ").grid(row=6, column=0, sticky="w", padx=(10, 6), pady=6)
cmb = ttk.Combobox(frm, textvariable=self.var_kind, values=["QR", "CODE128"], state="readonly", width=12)
cmb.grid(row=6, column=1, sticky="w", padx=(0, 10), pady=6)
cmb.bind("<<ComboboxSelected>>", lambda _e: self._update_preview())
chk = ttk.Checkbutton(frm, text="Rahmen um Labels zeichnen (Kalibrierung)", variable=self.var_border, command=self._update_preview)
chk.grid(row=6, column=2, columnspan=2, sticky="w", padx=(0, 10), pady=6)
cal = ttk.LabelFrame(frm, text="Kalibrierung (mm)")
cal.grid(row=7, column=0, columnspan=4, sticky="we", padx=10, pady=(10, 6))
for i in range(6):
cal.columnconfigure(i, weight=1)
def add_cal(label: str, var: tk.StringVar, col: int, hint: str) -> None:
ttk.Label(cal, text=label).grid(row=0, column=col, sticky="w", padx=(6, 4), pady=6)
e = ttk.Entry(cal, textvariable=var, width=10)
e.grid(row=0, column=col + 1, sticky="we", padx=(0, 10), pady=6)
e.bind("<KeyRelease>", lambda _e: self._update_preview())
ttk.Label(cal, text=hint, foreground="#555").grid(row=0, column=col + 2, sticky="w", padx=(0, 10), pady=6)
add_cal("Offset X", self.var_off_x, 0, "+ rechts")
add_cal("Offset Y", self.var_off_y, 3, "+ hoch")
def add_cal2(label: str, var: tk.StringVar, row: int, col: int, hint: str) -> None:
ttk.Label(cal, text=label).grid(row=row, column=col, sticky="w", padx=(6, 4), pady=6)
e = ttk.Entry(cal, textvariable=var, width=10)
e.grid(row=row, column=col + 1, sticky="we", padx=(0, 10), pady=6)
e.bind("<KeyRelease>", lambda _e: self._update_preview())
ttk.Label(cal, text=hint, foreground="#555").grid(row=row, column=col + 2, sticky="w", padx=(0, 10), pady=6)
add_cal2("Spalten-Pitch Δ", self.var_pitch_dx, 1, 0, "→ drift links/rechts")
add_cal2("Zeilen-Pitch Δ", self.var_pitch_dy, 1, 3, "→ top/bottom drift")
right = ttk.Frame(self)
right.grid(row=0, column=1, sticky="nsew", padx=(12, 0))
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
right.rowconfigure(1, weight=1)
ttk.Label(right, text="Vorschau (1 Label)").grid(row=0, column=0, sticky="w")
self.preview = ttk.Label(right)
self.preview.grid(row=1, column=0, sticky="nsew", pady=(6, 10))
btns = ttk.Frame(right)
btns.grid(row=2, column=0, sticky="we")
btns.columnconfigure(0, weight=1)
btns.columnconfigure(1, weight=1)
ttk.Button(btns, text="PDF erzeugen…", command=self.on_generate_pdf).grid(row=0, column=0, sticky="we", padx=(0, 6))
ttk.Button(btns, text="Ordner öffnen", command=self.on_open_folder).grid(row=0, column=1, sticky="we", padx=(6, 0))
self.lbl_status = ttk.Label(self, text="", foreground="#333")
self.lbl_status.grid(row=1, column=0, columnspan=2, sticky="we", pady=(10, 0))
def _apply_mode(self) -> None:
mode = self.var_mode.get()
if self._count_entry is not None:
self._count_entry.configure(state=("normal" if mode == "labels" else "disabled"))
if self._pages_entry is not None:
self._pages_entry.configure(state=("normal" if mode == "pages" else "disabled"))
self._update_preview()
def _parse_int(self, s: str, field: str) -> int:
try:
v = int(s.strip())
except Exception:
raise ValueError(f"{field} ist keine gültige Zahl.")
return v
def _parse_float(self, s: str, field: str) -> float:
try:
return float(s.strip().replace(",", "."))
except Exception:
raise ValueError(f"{field} ist keine gültige Zahl (z.B. 0.2).")
def _effective_count_and_pages(self) -> tuple[int, int]:
mode = self.var_mode.get()
if mode == "pages":
pages = self._parse_int(self.var_pages.get(), "Anzahl A4 Blätter")
if pages <= 0:
raise ValueError("Anzahl A4 Blätter muss > 0 sein.")
count = pages * AVERY_L4731.labels_per_page
return count, pages
else:
count = self._parse_int(self.var_count.get(), "Anzahl Labels")
if count <= 0:
raise ValueError("Anzahl Labels muss > 0 sein.")
pages = math.ceil(count / AVERY_L4731.labels_per_page)
return count, pages
def _current_text(self) -> str:
start = self._parse_int(self.var_start.get(), "Start-Nummer")
zeros = self._parse_int(self.var_zeros.get(), "Führende Nullen")
prefix = self.var_prefix.get().strip()
if not prefix:
raise ValueError("Prefix darf nicht leer sein.")
if start <= 0:
raise ValueError("Start-Nummer muss > 0 sein.")
if zeros < 0:
raise ValueError("Führende Nullen muss >= 0 sein.")
return make_asn_text(prefix, start, zeros)
def _calibration(self) -> tuple[float, float, float, float]:
off_x = self._parse_float(self.var_off_x.get(), "Offset X")
off_y = self._parse_float(self.var_off_y.get(), "Offset Y")
pdx = self._parse_float(self.var_pitch_dx.get(), "Spalten-Pitch Δ")
pdy = self._parse_float(self.var_pitch_dy.get(), "Zeilen-Pitch Δ")
return off_x, off_y, pdx, pdy
def _update_preview(self, *_args) -> None:
try:
text = self._current_text()
kind: BarcodeKind = "QR" if self.var_kind.get() == "QR" else "CODE128"
img = self._render_preview_image(text, kind, self.var_border.get())
self._preview_imgtk = ImageTk.PhotoImage(img)
self.preview.configure(image=self._preview_imgtk)
count, pages = self._effective_count_and_pages()
off_x, off_y, pdx, pdy = self._calibration()
self.lbl_status.configure(
text=f"Beispiel: {text} | {count} Labels ≈ {pages} Seiten | Offset({off_x},{off_y})mm PitchΔ({pdx},{pdy})mm"
)
except Exception as e:
self.preview.configure(image="")
self.lbl_status.configure(text=f"⚠️ {e}")
def _render_preview_image(self, text: str, kind: BarcodeKind, border: bool) -> Image.Image:
scale = 6
w = int((AVERY_L4731.label_w / mm) * scale * 10)
h = int((AVERY_L4731.label_h / mm) * scale * 10)
img = Image.new("RGB", (w, h), "white")
if kind == "QR":
size = int(h * 0.90)
qr = make_qr_image(text, max(120, size))
img.paste(qr, (10, (h - qr.size[1]) // 2))
else:
box = Image.new("RGB", (int(h * 0.90), int(h * 0.90)), "white")
img.paste(box, (10, (h - box.size[1]) // 2))
from PIL import ImageDraw, ImageFont
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("DejaVuSans.ttf", 18)
except Exception:
font = ImageFont.load_default()
draw.text((int(h * 0.95), (h - 18) // 2), text, fill="black", font=font)
if border:
draw.rectangle([0, 0, w - 1, h - 1], outline="black", width=2)
return img.resize((360, 160), Image.Resampling.LANCZOS)
def on_generate_pdf(self) -> None:
try:
start = self._parse_int(self.var_start.get(), "Start-Nummer")
zeros = self._parse_int(self.var_zeros.get(), "Führende Nullen")
prefix = self.var_prefix.get().strip()
if not prefix:
raise ValueError("Prefix darf nicht leer sein.")
count, _pages = self._effective_count_and_pages()
kind: BarcodeKind = "QR" if self.var_kind.get() == "QR" else "CODE128"
off_x, off_y, pdx, pdy = self._calibration()
default_name = f"asn_labels_{start}_{count}.pdf"
out = filedialog.asksaveasfilename(
title="PDF speichern",
defaultextension=".pdf",
initialfile=default_name,
filetypes=[("PDF", "*.pdf")],
)
if not out:
return
pages_generated, next_number = generate_pdf(
output_path=out,
start_number=start,
count=count,
prefix=prefix,
leading_zeros=zeros,
kind=kind,
layout=AVERY_L4731,
draw_border=self.var_border.get(),
offset_x_mm=off_x,
offset_y_mm=off_y,
pitch_dx_mm=pdx,
pitch_dy_mm=pdy,
)
self.lbl_status.configure(
text=f"✅ PDF: {out} | Seiten: {pages_generated} | Labels: {count} | Nächster Start: {next_number}"
)
self.var_start.set(str(next_number))
self._update_preview()
except Exception as e:
messagebox.showerror("Fehler", str(e))
def on_open_folder(self) -> None:
folder = os.getcwd()
try:
if os.name == "nt":
os.startfile(folder) # type: ignore[attr-defined]
elif os.name == "posix":
cmd = "open" if sys.platform == "darwin" else "xdg-open"
os.system(f'{cmd} "{folder}"')
except Exception:
messagebox.showinfo("Info", f"Ordner: {folder}")
def main() -> None:
root = tk.Tk()
try:
style = ttk.Style()
if "clam" in style.theme_names():
style.theme_use("clam")
except Exception:
pass
App(root)
root.minsize(900, 430)
root.mainloop()
if __name__ == "__main__":
main()