rechnungstool/bilder.py
TheMockTv 142dddc647 Bilder und Farbe gehoeren der Firma, dazu ein Vorschau-Knopf
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
2026-09-05 17:35:37 +02:00

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