commit cef8539e00994a72afa2bd97bb0a9c8a50b2cba3 Author: hubaceks Date: Wed May 13 22:41:42 2026 +0200 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..90799c4 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..430173c --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Generátor katalogu + +Generuje PDF katalog z YAML nebo CSV dat přes HTML/CSS šablonu. + +## Instalace + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\\Scripts\\activate +pip install -r requirements.txt +``` + +> Poznámka: WeasyPrint může na některých systémech vyžadovat systémové knihovny. Pokud by instalace zlobila, lze později přepnout renderer na Playwright/Chromium. + +## Použití + +Z YAML: +```bash +python generate_catalog.py --input catalog.yaml +``` + +Z CSV: +```bash +python generate_catalog.py --input catalog.csv --title "BistroUšky" --subtitle "CAKE AND PATISSERIE" +``` + +Výstup je standardně `output/katalog.pdf`. + +## Struktura dat + +- `brand`: název, podtitulek, logo, pozadí, ikony +- `settings`: rozložení, počet sloupců, výstupní soubor +- `categories`: seznam kategorií +- `products`: produkty v kategoriích + +Každý produkt má minimálně: +- `name` +- `price` +- `image` + +Volitelně: +- `note` +- `tags` +- `show_allergen_icon` diff --git a/README_FRONTEND.md b/README_FRONTEND.md new file mode 100644 index 0000000..9649488 --- /dev/null +++ b/README_FRONTEND.md @@ -0,0 +1,81 @@ +# Webový frontend pro generátor katalogu + +## Instalace na Debianu + +```bash +apt update +apt install -y \ + python3 python3-venv python3-pip \ + libpango-1.0-0 libpangoft2-1.0-0 libcairo2 \ + libgdk-pixbuf-2.0-0 libffi8 shared-mime-info fonts-dejavu-core +``` + +## Spuštění + +```bash +cd /opt/catalog_generator +source venv/bin/activate +pip install -r requirements.txt +python app.py +``` + +Pak otevři: + +```text +http://192.168.50.112:8080 +``` + +## Formát CSV + +Povinné sloupce: + +```csv +category,name,price,image +Klasické zákusky,Věneček,"85,-",venecek.jpg +Dortíky,Red Velvet,"145,-",red-velvet.jpg +``` + +Volitelné sloupce: + +```csv +note,tags,show_allergen_icon +``` + +Ve sloupci `image` může být jen název souboru (`venecek.jpg`) nebo cesta `images/venecek.jpg`. + +## Nahrávání fotek + +Frontend podporuje: + +- více samostatných obrázků najednou, +- jeden ZIP archiv s obrázky. + +Podporované formáty: JPG, JPEG, PNG, WebP. + +## Systemd služba + +Soubor `/etc/systemd/system/catalog-generator.service`: + +```ini +[Unit] +Description=Catalog Generator Web App +After=network.target + +[Service] +WorkingDirectory=/opt/catalog_generator +ExecStart=/opt/catalog_generator/venv/bin/python /opt/catalog_generator/app.py +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +``` + +Aktivace: + +```bash +systemctl daemon-reload +systemctl enable --now catalog-generator +systemctl status catalog-generator +``` diff --git a/__pycache__/generate_catalog.cpython-313.pyc b/__pycache__/generate_catalog.cpython-313.pyc new file mode 100644 index 0000000..6d73ed5 Binary files /dev/null and b/__pycache__/generate_catalog.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..3713ade --- /dev/null +++ b/app.py @@ -0,0 +1,96 @@ +from pathlib import Path +import shutil +import subprocess +import uuid +import zipfile + +from flask import Flask, render_template, request, send_file, abort + +BASE_DIR = Path(__file__).resolve().parent +UPLOAD_DIR = BASE_DIR / "uploads" +OUTPUT_DIR = BASE_DIR / "web_output" + +UPLOAD_DIR.mkdir(exist_ok=True) +OUTPUT_DIR.mkdir(exist_ok=True) + +app = Flask(__name__) + + +@app.get("/") +def index(): + return render_template("upload.html") + + +@app.post("/generate") +def generate(): + job_id = uuid.uuid4().hex + job_dir = UPLOAD_DIR / job_id + images_dir = job_dir / "images" + job_dir.mkdir(parents=True) + images_dir.mkdir(parents=True) + + csv_file = request.files.get("csv_file") + if not csv_file or not csv_file.filename: + return "Chybí CSV soubor", 400 + + csv_path = job_dir / "catalog.csv" + csv_file.save(csv_path) + + for image in request.files.getlist("images"): + if image and image.filename: + image.save(images_dir / Path(image.filename).name) + + zip_file = request.files.get("images_zip") + if zip_file and zip_file.filename: + zip_path = job_dir / "images.zip" + zip_file.save(zip_path) + with zipfile.ZipFile(zip_path, "r") as z: + z.extractall(images_dir) + + output_pdf = OUTPUT_DIR / f"catalog_{job_id}.pdf" + + cmd = [ + "python", + str(BASE_DIR / "generate_catalog.py"), + "-i", + str(csv_path), + "--output", + str(output_pdf), + "--images-dir", + str(images_dir), + ] + + result = subprocess.run( + cmd, + cwd=BASE_DIR, + text=True, + capture_output=True, + ) + + if result.returncode != 0: + return ( + "

Generování selhalo

" + "

STDOUT

" + result.stdout + "
" + "

STDERR

" + result.stderr + "
" + ), 500 + + if not output_pdf.exists(): + return "PDF se nevytvořilo.", 500 + + return f""" +

Katalog je hotový

+

Stáhnout PDF

+

Vygenerovat další

+ """ + + +@app.get("/download/") +def download(job_id): + pdf_path = OUTPUT_DIR / f"catalog_{job_id}.pdf" + if not pdf_path.exists(): + abort(404) + return send_file(pdf_path, as_attachment=True, download_name="catalog.pdf") + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/assets/allergen.png b/assets/allergen.png new file mode 100644 index 0000000..a9a8c97 Binary files /dev/null and b/assets/allergen.png differ diff --git a/assets/background.png b/assets/background.png new file mode 100755 index 0000000..d04b5ad Binary files /dev/null and b/assets/background.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..d71d482 Binary files /dev/null and b/assets/logo.png differ diff --git a/catalog.csv b/catalog.csv new file mode 100644 index 0000000..7924b57 --- /dev/null +++ b/catalog.csv @@ -0,0 +1,4 @@ +category,name,price,image,show_allergen_icon,note +Klasické zákusky,Likérová špička,"103,-",images/likerova_spicka.jpg,true, +Klasické zákusky,Laskonka - mini,"45,-",images/laskonka_mini.jpg,true, +Dortíky,Red Velvet,"145,-",images/red_velvet.jpg,true, diff --git a/catalog.yaml b/catalog.yaml new file mode 100644 index 0000000..ad1ee85 --- /dev/null +++ b/catalog.yaml @@ -0,0 +1,35 @@ +brand: + title: "BistroUšky" + subtitle: "CAKE AND PATISSERIE" + cover_logo: "static/logo.png" # volitelné + background: "static/background.jpg" # volitelné, použije se na každé stránce + allergen_icon: "static/allergen_icon.png" # volitelné + +settings: + page_size: "landscape-a4" # landscape-a4 / portrait-a4 + products_per_page: 8 # při 4 sloupcích a 2 řádcích + columns: 4 + show_prices: true + currency_suffix: "" + output_file: "output/katalog.pdf" + +categories: + - name: "Klasické zákusky" + products: + - name: "Likérová špička" + price: "103,-" + image: "images/likerova_spicka.jpg" + tags: [] + note: "" + show_allergen_icon: true + - name: "Laskonka - mini" + price: "45,-" + image: "images/laskonka_mini.jpg" + show_allergen_icon: true + + - name: "Dortíky" + products: + - name: "Red Velvet" + price: "145,-" + image: "images/red_velvet.jpg" + show_allergen_icon: true diff --git a/generate_catalog.py b/generate_catalog.py new file mode 100644 index 0000000..98fb6c9 --- /dev/null +++ b/generate_catalog.py @@ -0,0 +1,184 @@ +from pathlib import Path +import argparse +import csv + +from jinja2 import Environment, FileSystemLoader +from weasyprint import HTML + + +def load_csv(csv_path): + categories = {} + + with open(csv_path, newline="", encoding="utf-8-sig") as f: + sample = f.read(4096) + f.seek(0) + + dialect = csv.Sniffer().sniff(sample, delimiters=",;") + reader = csv.DictReader(f, dialect=dialect) + + for row in reader: + normalized = { + (key or "").strip().lower().replace(" ", "_"): (value or "").strip() + for key, value in row.items() + } + + category = ( + normalized.get("category") + or normalized.get("catagory") + or normalized.get("kategorie") + or "Ostatní" + ) + + name = ( + normalized.get("name") + or normalized.get("nazev") + or normalized.get("název") + or "" + ) + + price = ( + normalized.get("price") + or normalized.get("cena") + or "" + ) + + image = ( + normalized.get("image") + or normalized.get("foto") + or normalized.get("fotka") + or normalized.get("obrazek") + or normalized.get("obrázek") + or "" + ) + + show_allergen_icon = ( + normalized.get("show_alergen_icon") + or normalized.get("show_allergen_icon") + or normalized.get("alergen_icon") + or normalized.get("allergen_icon") + or "" + ).lower() in ("1", "true", "yes", "ano", "y") + + note = ( + normalized.get("note") + or normalized.get("description") + or normalized.get("poznamka") + or normalized.get("poznámka") + or "" + ) + + if category not in categories: + categories[category] = [] + + categories[category].append({ + "name": name, + "price": price, + "image": image, + "show_allergen_icon": show_allergen_icon, + "description": note, + }) + + return categories + + +def build_catalog_data(title, subtitle, categories): + return { + "title": title, + "subtitle": subtitle, + "categories": [ + { + "name": name, + "products": products, + } + for name, products in categories.items() + ], + } + + +def render_html(data, template_dir, images_dir): + env = Environment( + loader=FileSystemLoader(template_dir) + ) + + template = env.get_template("catalog.html.j2") + return template.render( + catalog=data, + brand=data, + settings={ + "columns": 4, + "page_size": "1024px 768px", + "margin": "0", + }, + images_dir=images_dir.as_posix(), + assets_dir=(Path(__file__).resolve().parent / "assets").as_posix(), + logo_exists=(Path(__file__).resolve().parent / "assets" / "logo.png").exists(), +) + + +def generate_pdf(html_content, output_path): + HTML(string=html_content).write_pdf(output_path) + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "-i", + "--input", + required=True, + help="CSV input file" + ) + + parser.add_argument( + "--output", + default="output/catalog.pdf", + help="Output PDF path" + ) + + parser.add_argument( + "--images-dir", + default="images", + help="Directory with product images" + ) + + parser.add_argument( + "--title", + default="Bistro Ušky", + ) + + parser.add_argument( + "--subtitle", + default="Cake & Patisserie", + ) + + args = parser.parse_args() + + base_dir = Path(__file__).resolve().parent + + csv_path = Path(args.input) + output_path = Path(args.output) + images_dir = Path(args.images_dir) + + output_path.parent.mkdir(parents=True, exist_ok=True) + + categories = load_csv(csv_path) + + catalog_data = build_catalog_data( + args.title, + args.subtitle, + categories, + ) + + html = render_html( + catalog_data, + base_dir / "templates", + images_dir, + ) + + generate_pdf(html, output_path) + + print(f"PDF generated: {output_path}") + + +if __name__ == "__main__": + main() diff --git a/output/katalog.html b/output/katalog.html new file mode 100644 index 0000000..eb92758 --- /dev/null +++ b/output/katalog.html @@ -0,0 +1,236 @@ + + + + + BistroUšky + + + +
+
+

BistroUšky

+
CAKE AND PATISSERIE
+ +
+
+ + +
+

+ Klasické zákusky +

+ +
+ +
+
+ + Likérová špička + +
+
+
Likérová špička
+ + ikona + +
+
103,-
+ +
+ +
+
+ + Laskonka - mini + +
+
+
Laskonka - mini
+ + ikona + +
+
45,-
+ +
+ +
+
+ +
+

+ Dortíky +

+ +
+ +
+
+ + Red Velvet + +
+
+
Red Velvet
+ + ikona + +
+
145,-
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/output/katalog.pdf b/output/katalog.pdf new file mode 100644 index 0000000..95102d9 Binary files /dev/null and b/output/katalog.pdf differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e2a4fc1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +jinja2>=3.1.0 +pyyaml>=6.0.0 +weasyprint>=62.0 +pillow>=10.0.0 +flask>=3.0.0 +werkzeug>=3.0.0 diff --git a/static/background.png b/static/background.png new file mode 100755 index 0000000..d04b5ad Binary files /dev/null and b/static/background.png differ diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000..d71d482 Binary files /dev/null and b/static/logo.png differ diff --git a/templates/catalog.html.j2 b/templates/catalog.html.j2 new file mode 100644 index 0000000..bdc8fea --- /dev/null +++ b/templates/catalog.html.j2 @@ -0,0 +1,187 @@ + + + + + + + + + +
+
{{ catalog.title }}
+
{{ catalog.subtitle|upper }}
+ + {% if logo_exists %} + + {% endif %} +
+ +{% for category in catalog.categories %} +
+

{{ category.name }}

+ +
+ {% for product in category.products %} +
+ {% if product.image %} + + {% else %} +
bez fotky
+ {% endif %} + +
+
{{ product.name }}
+ {% if product.show_allergen_icon %} + + {% endif %} +
+ +
{{ product.price }}
+
+ {% endfor %} +
+
+{% endfor %} + + + diff --git a/templates/upload.html b/templates/upload.html new file mode 100644 index 0000000..4d459a0 --- /dev/null +++ b/templates/upload.html @@ -0,0 +1,64 @@ + + + + + Generátor katalogu + + + +
+

Generátor katalogu

+

Nahraj CSV a fotky produktů. Názvy fotek musí odpovídat sloupci image v CSV.

+ +
+ + + + + + + + + + +
+
+ + diff --git a/uploads/.DS_Store b/uploads/.DS_Store new file mode 100644 index 0000000..0d9c349 Binary files /dev/null and b/uploads/.DS_Store differ diff --git a/web_jobs/.DS_Store b/web_jobs/.DS_Store new file mode 100644 index 0000000..c08e6fc Binary files /dev/null and b/web_jobs/.DS_Store differ diff --git a/web_jobs/dda19e80c065/.DS_Store b/web_jobs/dda19e80c065/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/web_jobs/dda19e80c065/.DS_Store differ diff --git a/web_output/.DS_Store b/web_output/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/web_output/.DS_Store differ