Files
orderPRO/tools/generowanie/buteleczki_klaudia_buczma_batch.py
2026-05-11 20:22:04 +02:00

276 lines
8.8 KiB
Python

"""
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()