276 lines
8.8 KiB
Python
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()
|