Logo und ein Fussstreifen sind austauschbar (bilder.py: das gewaehlte Bild wird KOPIERT, ein Pfad auf den Desktop waere morgen tot), die Akzentfarbe kommt aus den Einstellungen - Hex-Feld und Farbwaehler. Firmenname, Anschrift, Bank und Steuernummer bleiben gezeichnet; kein Bild kann eine Pflichtangabe verdecken. Der Vorschau-Knopf zeigt das Blatt, bevor es eine Rechnung wird: kein Nummernverbrauch, nichts im Rechnungsordner, "VORSCHAU - keine Rechnung" quer ueber jeder Seite. Sonst kostet jeder Tippfehler einen Storno. Neue Pruefstaende: pruef_bilder.py, pruef_vorschau.py Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Bilder fuer Kopf- und Fusszeile der Rechnung: ablegen, wiederfinden, laden.
|
|
|
|
Warum eine eigene Datei: Kopfbild und Fussbild brauchen dieselben drei Schritte
|
|
(uebernehmen -> aufloesen -> laden), und der PDF-Renderer, der Einstellungsdialog
|
|
und die Pruefstaende brauchen sie alle. Stuende die Logik im Renderer, muesste der
|
|
Dialog sie nachbauen - und beim naechsten Bild laufen die beiden auseinander.
|
|
|
|
⭐ Der wichtigste Teil ist `uebernehmen`: das gewaehlte Bild wird in den Ordner
|
|
`bilder/` neben der config.json KOPIERT, statt nur seinen Pfad zu merken. Sonst
|
|
zeigt die Einstellung auf den Desktop des Nutzers - und sobald er dort aufraeumt,
|
|
faellt das Logo still von der Rechnung. Gemerkt wird nur der kurze Name
|
|
("bilder/logo.png"), damit die Einstellung auch nach einem Umzug des Ordners passt.
|
|
"""
|
|
import os
|
|
import shutil
|
|
|
|
import config
|
|
|
|
# Erlaubte Formate. reportlab liest PNG/JPG/GIF direkt; SVG kann es nicht -
|
|
# deshalb steht es hier bewusst NICHT.
|
|
FORMATE = (".png", ".jpg", ".jpeg", ".gif")
|
|
ORDNER = "bilder"
|
|
|
|
|
|
def bilder_dir():
|
|
"""Ordner fuer die uebernommenen Bilder (neben der config.json). Wird angelegt."""
|
|
d = os.path.join(config.BASE_DIR, ORDNER)
|
|
os.makedirs(d, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def uebernehmen(quelle):
|
|
"""Bild in den Bilderordner kopieren -> gibt den zu speichernden Pfad zurueck.
|
|
|
|
Liegt die Datei schon im Bilderordner, wird nichts kopiert. Ein Name, der
|
|
bereits vergeben ist, bekommt eine Nummer angehaengt - ein bestehendes Bild
|
|
wird NIE ueberschrieben, sonst waere das alte Logo mit einem Klick weg.
|
|
"""
|
|
quelle = (quelle or "").strip()
|
|
if not quelle:
|
|
return ""
|
|
if not os.path.exists(quelle):
|
|
print(f"[bilder] Quelle gibt es nicht: {quelle}")
|
|
return ""
|
|
ziel_dir = bilder_dir()
|
|
name = os.path.basename(quelle)
|
|
if os.path.dirname(os.path.abspath(quelle)) == os.path.abspath(ziel_dir):
|
|
return f"{ORDNER}/{name}"
|
|
stamm, endung = os.path.splitext(name)
|
|
ziel = os.path.join(ziel_dir, name)
|
|
n = 2
|
|
while os.path.exists(ziel) and not _gleich(quelle, ziel):
|
|
ziel = os.path.join(ziel_dir, f"{stamm}-{n}{endung}")
|
|
n += 1
|
|
try:
|
|
shutil.copyfile(quelle, ziel)
|
|
print(f"[bilder] uebernommen: {quelle} -> {ziel}")
|
|
except OSError as e:
|
|
print(f"[bilder] FEHLER beim Kopieren ({e}) - merke den Originalpfad")
|
|
return quelle
|
|
return f"{ORDNER}/{os.path.basename(ziel)}"
|
|
|
|
|
|
def _gleich(a, b):
|
|
"""Zwei Dateien byte-gleich? Verhindert 'logo-2.png' beim zweiten Waehlen desselben Bilds."""
|
|
try:
|
|
if os.path.getsize(a) != os.path.getsize(b):
|
|
return False
|
|
with open(a, "rb") as fa, open(b, "rb") as fb:
|
|
return fa.read() == fb.read()
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def aufloesen(pfad):
|
|
"""Gespeicherten Pfad -> existierender absoluter Pfad, sonst "".
|
|
|
|
Ein relativer Pfad wird erst neben dem Programm gesucht (BASE_DIR, dort liegt
|
|
der Bilderordner), dann im gebuendelten Ressourcenordner der .exe - so findet
|
|
die exe ihr mitgeliefertes Standardlogo, der Nutzer aber sein eigenes.
|
|
"""
|
|
pfad = (pfad or "").strip()
|
|
if not pfad:
|
|
return ""
|
|
if os.path.isabs(pfad):
|
|
return pfad if os.path.exists(pfad) else ""
|
|
for basis in (config.BASE_DIR, config.RESOURCE_DIR):
|
|
kandidat = os.path.join(basis, pfad)
|
|
if os.path.exists(kandidat):
|
|
return kandidat
|
|
return ""
|
|
|
|
|
|
def laden(pfad):
|
|
"""(ImageReader, breite, hoehe) oder (None, 0, 0). Meldet, warum es nicht ging."""
|
|
voll = aufloesen(pfad)
|
|
if not voll:
|
|
if (pfad or "").strip():
|
|
print(f"[bilder] nicht gefunden: {pfad}")
|
|
return None, 0, 0
|
|
try:
|
|
from reportlab.lib.utils import ImageReader
|
|
img = ImageReader(voll)
|
|
iw, ih = img.getSize()
|
|
return img, iw, ih
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"[bilder] '{voll}' laesst sich nicht lesen: {e}")
|
|
return None, 0, 0
|
|
|
|
|
|
def einpassen(iw, ih, box_w, box_h):
|
|
"""Groesse im Kasten, Seitenverhaeltnis bleibt. Nie groesser als das Original."""
|
|
if not iw or not ih:
|
|
return 0, 0
|
|
skala = min(box_w / iw, box_h / ih)
|
|
return iw * skala, ih * skala
|