This commit is contained in:
2026-05-11 20:22:04 +02:00
parent 2b4d843ae0
commit 0578bfc2ae
10 changed files with 591 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
"""Inspekcja PSD: wejscie do kazdego SO i wypisanie jego warstw."""
import os
import subprocess
import time
import photoshop.api as ps
PSD = r"d:\pomysloweprezenty.pl\projekty\buteleczki - indywidualny wzór\Klaudia Buczma.psd"
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("brak PS")
def open_smart_object(app):
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 walk(layer, prefix=""):
try:
name = layer.name
except Exception:
name = "?"
kind = type(layer).__name__
extra = ""
try:
if hasattr(layer, "kind"):
extra = f" kind={layer.kind}"
except Exception:
pass
try:
bounds = [float(b) for b in layer.bounds]
extra += f" bounds={bounds}"
except Exception:
pass
content = ""
try:
if hasattr(layer, "textItem"):
content = f" text=\"{layer.textItem.contents}\""
except Exception:
pass
print(f"{prefix}- {name} [{kind}]{extra}{content}")
for attr in ("layerSets", "artLayers"):
try:
for c in getattr(layer, attr):
walk(c, prefix + " ")
except Exception:
pass
def dump_doc(doc, label):
print(f"=== {label}: {doc.name} ===")
for ls in doc.layerSets:
walk(ls, "")
for al in doc.artLayers:
walk(al, "")
def main():
app = ensure_photoshop()
doc = app.open(PSD)
dump_doc(doc, "ROOT")
so_names = ["Warstwa 2", "Warstwa 2 kopia", "Warstwa 2 kopia 2", "Warstwa 2 kopia 3"]
for so_name in so_names:
try:
target = doc.artLayers[so_name]
except Exception:
print(f"!! brak warstwy {so_name}")
continue
doc.activeLayer = target
try:
open_smart_object(app)
except Exception as e:
print(f"!! nie udalo sie wejsc do {so_name}: {e}")
continue
so_doc = app.activeDocument
dump_doc(so_doc, f"SO[{so_name}]")
so_doc.close(ps.SaveOptions.DoNotSaveChanges)
doc.close(ps.SaveOptions.DoNotSaveChanges)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
SET NAMES utf8mb4;
UPDATE order_items SET project_generated = 1, project_generated_at = NOW() WHERE id IN (5911, 5912);
SELECT id, project_generated, project_generated_at FROM order_items WHERE id IN (5911, 5912);

View File

@@ -0,0 +1,5 @@
SET NAMES utf8mb4;
SELECT oi.id, oi.original_name, oi.quantity, oi.project_generated
FROM order_items oi
WHERE oi.order_id = 1009
ORDER BY oi.id;

View File

@@ -0,0 +1,8 @@
SET NAMES utf8mb4;
SHOW COLUMNS FROM orders;
SELECT '---';
SHOW COLUMNS FROM order_items;
SELECT '---';
SELECT * FROM order_items WHERE id = 5898\G
SELECT '---';
SELECT * FROM orders WHERE id = 1009\G

View File

@@ -0,0 +1,27 @@
SET NAMES utf8mb4;
SELECT
oi.id AS item_id,
oi.original_name AS product_name,
oi.personalization,
oi.quantity,
o.id AS order_id,
o.internal_order_number,
o.status_code AS status_code,
oa.name AS buyer_name,
oa.email AS buyer_email,
pm.script_name,
pm.output_dir,
pm.requires_photo,
(SELECT GROUP_CONCAT(n.comment SEPARATOR '\n---\n') FROM order_notes n WHERE n.order_id = o.id AND n.note_type = 'message') AS customer_message
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN order_statuses os ON o.status_code = os.code
JOIN order_status_groups osg ON os.group_id = osg.id
JOIN project_mappings pm ON pm.is_active = 1 AND oi.original_name = pm.product_name_pattern
LEFT JOIN order_addresses oa ON o.id = oa.order_id AND oa.address_type = 'customer'
WHERE osg.id = 2
AND oi.project_generated = 0
AND oi.personalization IS NOT NULL
AND oi.personalization <> ''
AND oa.name LIKE '%Żarnowska%'
ORDER BY o.id;

View File

@@ -0,0 +1,3 @@
SET NAMES utf8mb4;
UPDATE order_items SET quantity = 1 WHERE id = 5898;
SELECT id, original_name, quantity FROM order_items WHERE id = 5898;

View File

@@ -0,0 +1,3 @@
SET NAMES utf8mb4;
UPDATE orders SET total_with_tax = 89.00, total_paid = 89.00 WHERE id = 1009;
SELECT id, internal_order_number, total_with_tax, total_paid FROM orders WHERE id = 1009;

View File

@@ -0,0 +1,275 @@
"""
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()