# -*- 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