""" Generator wsadowy PSD: buteleczki "Klaudia Buczma" (4 gosci per PSD). Otwiera szablon `Klaudia Buczma.psd`, dla kazdej z 4 warstw Smart Object (Warstwa 2, Warstwa 2 kopia, Warstwa 2 kopia 2, Warstwa 2 kopia 3) wchodzi do srodka i podmienia warstwe tekstowa `gosc` (sciezka: Warstwa 4 -> gosc). Zapisuje jako `Klaudia Buczma NN.psd` w `_gotowe/`. Lista gosci pobierana z .docx (komorki tabeli). Grupy < 4 osob -> brakujace SO sa ukrywane (visible=False). Uzycie: python buteleczki_klaudia_buczma_batch.py # wszystkie grupy python buteleczki_klaudia_buczma_batch.py --only 1 # tylko pierwsza grupa (test) python buteleczki_klaudia_buczma_batch.py --from 5 --to 7 # zakres grup """ import argparse import os import subprocess import sys import time import photoshop.api as ps from docx import Document PROJEKT_DIR = r"d:\pomysloweprezenty.pl\projekty\buteleczki - indywidualny wzór" SZABLON_PATH = os.path.join(PROJEKT_DIR, "Klaudia Buczma.psd") GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe_edytowalne") DOCX_PATH = r"d:\temp\pomysloweprezenty.pl\buteleczki.docx" SO_LAYER_NAMES = [ "Warstwa 2 kopia 2", # lewy gora "Warstwa 2 kopia 3", # prawy gora "Warstwa 2 kopia", # lewy dol "Warstwa 2", # prawy dol ] # Sciezka do warstwy `gosc` wewnatrz SO SO_GROUP_NAME = "Warstwa 4" GOSC_LAYER_NAME = "gosc" PHOTOSHOP_EXE_CANDIDATES = [ r"C:\Program Files\Adobe\Adobe Photoshop 2024\Photoshop.exe", r"C:\Program Files\Adobe\Adobe Photoshop 2023\Photoshop.exe", r"C:\Program Files\Adobe\Adobe Photoshop 2025\Photoshop.exe", r"C:\Program Files\Adobe\Adobe Photoshop 2026\Photoshop.exe", ] def ensure_photoshop(): try: return ps.Application() except Exception: for exe in PHOTOSHOP_EXE_CANDIDATES: if os.path.exists(exe): subprocess.Popen([exe]) break for _ in range(30): time.sleep(2) try: return ps.Application() except Exception: continue raise RuntimeError("Nie udalo sie uruchomic Photoshopa") def open_smart_object(app): """Otwiera zawartosc aktywnej warstwy Smart Object do edycji.""" desc = ps.ActionDescriptor() ref = ps.ActionReference() ref.putEnumerated( app.stringIDToTypeID("layer"), app.stringIDToTypeID("ordinal"), app.stringIDToTypeID("targetEnum"), ) desc.putReference(app.stringIDToTypeID("null"), ref) app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc) def delete_active_layer(app): """Usuwa aktywna warstwe (przez action manager - dziala na surowych COM).""" desc = ps.ActionDescriptor() ref = ps.ActionReference() ref.putEnumerated( app.stringIDToTypeID("layer"), app.stringIDToTypeID("ordinal"), app.stringIDToTypeID("targetEnum"), ) desc.putReference(app.stringIDToTypeID("null"), ref) app.executeAction(app.stringIDToTypeID("delete"), desc) def smart_object_make_copy(app): """Layer > Smart Objects > New Smart Object Via Copy. Odlacza aktywny SO od pozostalych zlinkowanych instancji.""" app.executeAction( app.stringIDToTypeID("placedLayerMakeCopy"), ps.ActionDescriptor() ) def set_text(layer, new_text): """Zmienia tekst zachowujac srodek bounding boxa.""" b = [float(x) for x in layer.bounds] cx_before = (b[0] + b[2]) / 2 cy_before = (b[1] + b[3]) / 2 layer.textItem.contents = new_text b2 = [float(x) for x in layer.bounds] cx_after = (b2[0] + b2[2]) / 2 cy_after = (b2[1] + b2[3]) / 2 dx = cx_before - cx_after dy = cy_before - cy_after if dx or dy: layer.translate(dx, dy) def split_two_lines(name): """Lamie nazwe na 2 linie - przy spacji najblizszej srodkowi. Dla 1-slowowych zwraca bez zmian.""" parts = name.split(" ") if len(parts) < 2: return name mid = len(name) / 2 best_i = None best_dist = None pos = 0 for i, part in enumerate(parts[:-1]): pos += len(part) dist = abs(pos - mid) if best_dist is None or dist < best_dist: best_dist = dist best_i = i pos += 1 # spacja left = " ".join(parts[: best_i + 1]) right = " ".join(parts[best_i + 1 :]) return f"{left}\r{right}" def read_names(docx_path): doc = Document(docx_path) names = [] for t in doc.tables: for row in t.rows: for cell in row.cells: txt = cell.text.strip() if txt: names.append(txt) return names def chunk4(items): for i in range(0, len(items), 4): yield items[i:i + 4] def find_layer_by_name(container, name): """Rekurencyjnie szuka warstwy po nazwie w doc/layerSet.""" for ls in container.layerSets: if ls.name == name: return ls found = find_layer_by_name(ls, name) if found is not None: return found for al in container.artLayers: if al.name == name: return al return None def edit_so_gosc(app, doc, so_layer_name, gosc_text): """Wchodzi do SO, podmienia gosc, zapisuje i wraca.""" target = find_layer_by_name(doc, so_layer_name) if target is None: raise RuntimeError(f"Nie znaleziono warstwy {so_layer_name}") doc.activeLayer = target # 4 SO w szablonie sa zlinkowane (wspolny kontener). "New Smart Object Via # Copy" tworzy niezalezna kopie obok zrodla. Kasujemy oryginal i # przemianowujemy kopie - efekt: ta sama warstwa w tym samym miejscu, # ale juz odpieta od pozostalych instancji. smart_object_make_copy(app) independent = doc.activeLayer independent_name_tmp = independent.name # Skasuj oryginal (target) - aktywuj go i usun przez action manager doc.activeLayer = target delete_active_layer(app) # Wroc do niezaleznej kopii i nadaj jej nazwe oryginalu doc.activeLayer = independent independent.name = so_layer_name open_smart_object(app) so_doc = app.activeDocument gosc_layer = find_layer_by_name(so_doc, GOSC_LAYER_NAME) if gosc_layer is None: raise RuntimeError(f"Nie znaleziono warstwy '{GOSC_LAYER_NAME}' w SO {so_layer_name}") old = gosc_layer.textItem.contents set_text(gosc_layer, gosc_text) so_doc.save() so_doc.close() print(f" {so_layer_name}: '{old.strip()}' -> '{gosc_text}'") def generate_group(app, names_group, index): """Generuje jeden PSD dla grupy do 4 nazwisk.""" os.makedirs(GOTOWE_DIR, exist_ok=True) out_name = f"Klaudia Buczma {index:02d}.psd" out_path = os.path.join(GOTOWE_DIR, out_name) doc = app.open(SZABLON_PATH) print(f"[{index:02d}] otwarto szablon ({len(names_group)} osob)") for slot_idx, so_name in enumerate(SO_LAYER_NAMES): if slot_idx < len(names_group): edit_so_gosc(app, doc, so_name, split_two_lines(names_group[slot_idx])) else: doc.artLayers[so_name].visible = False print(f" {so_name}: UKRYTE (brak osoby)") psd_opts = ps.PhotoshopSaveOptions() doc.saveAs(out_path, psd_opts, True) print(f"[{index:02d}] zapisano: {out_path}") doc.close(ps.SaveOptions.DoNotSaveChanges) def main(): parser = argparse.ArgumentParser() parser.add_argument("--only", type=int, help="Tylko jedna grupa (1-based)") parser.add_argument("--from", dest="from_", type=int, default=1, help="Od grupy N") parser.add_argument("--to", type=int, help="Do grupy N (wlacznie)") parser.add_argument( "--names", help="Lista nazwisk oddzielona ';' (1-4 sztuk). Generuje jeden PSD.", ) parser.add_argument( "--index", type=int, help="Numer wynikowego PSD przy uzyciu --names (np. 24 -> 'Klaudia Buczma 24.psd')", ) args = parser.parse_args() app = ensure_photoshop() while app.documents.length > 0: app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges) if args.names: if args.index is None: raise SystemExit("--names wymaga --index N") custom = [n.strip() for n in args.names.split(";") if n.strip()] if not (1 <= len(custom) <= 4): raise SystemExit("--names: 1-4 nazwisk oddzielonych ';'") generate_group(app, custom, args.index) print("Gotowe!") return names = read_names(DOCX_PATH) groups = list(chunk4(names)) print(f"Wczytano {len(names)} osob -> {len(groups)} grup po 4") if args.only: indices = [args.only] else: end = args.to or len(groups) indices = list(range(args.from_, end + 1)) for idx in indices: if not (1 <= idx <= len(groups)): print(f"Pominieto grupe {idx} (poza zakresem)") continue generate_group(app, groups[idx - 1], idx) print("Gotowe!") if __name__ == "__main__": main()