commit 4f2723b7670167aa3cb3915fbe50d23a53ddefa0 Author: Aron Date: Thu Sep 11 16:01:32 2025 +0200 initial webapp draft diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..963a057 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..33730b6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Dockerfile +FROM python:3.12-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set working directory +WORKDIR /cost-assistant + +# Copy requirements first (for caching) +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the project files +COPY . . + +# Create upload folder +RUN mkdir -p data/uploads + +# Expose port +EXPOSE 8000 + +# Run FastAPI with Uvicorn +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6634c5 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# papercut-lite-printshop + +Minimal Flask-based PDF print-cost calculator with optional ink-coverage adjustment. + +Run: + +```bash +docker-compose up --build +``` + +Then open http://localhost:8000 and upload PDFs. Configure rate via env var RATE_PER_M2 (default 4.0). \ No newline at end of file diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..2aab232 Binary files /dev/null and b/__pycache__/main.cpython-311.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..4c1c65a Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/cost_calculator.py b/cost_calculator.py new file mode 100644 index 0000000..7fc1cf0 --- /dev/null +++ b/cost_calculator.py @@ -0,0 +1,87 @@ +# cost_calculator.py +import os +from PyPDF2 import PdfReader +from pdf2image import convert_from_path +import numpy as np + +UPLOAD_FOLDER = "data/uploads" +ALLOWED_EXTENSIONS = {"pdf"} + + +def allowed_file(filename: str) -> bool: + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +def points_to_meters(points: float) -> float: + return (points / 72.0) * 0.0254 + + +def get_rate_black() -> float: + return float(os.environ.get("RATE_PER_M2_BLACK", "4.0")) + + +def get_rate_color() -> float: + return float(os.environ.get("RATE_PER_M2_COLOR", "5.0")) + + +def analyze_pdf(path, compute_ink=True): + reader = PdfReader(path) + pages_info = [] + total_area_black = total_area_color = 0.0 + total_cost_black = total_cost_color = 0.0 + + for i, page in enumerate(reader.pages): + box = page.mediabox + width_m = points_to_meters(float(box.width)) + height_m = points_to_meters(float(box.height)) + area = width_m * height_m + + ink_pct = None + is_color = False + + if compute_ink: + try: + images = convert_from_path(path, first_page=i + 1, last_page=i + 1, dpi=150) + img = images[0].convert("RGB") + arr = np.array(img) + + # ink pixels: any channel < 240 + ink_mask = np.any(arr < 240, axis=2) + ink_pct = float(np.count_nonzero(ink_mask)) / (arr.shape[0] * arr.shape[1]) * 100.0 + + # simple color detection: if RGB channels differ significantly + avg_rgb = arr.mean(axis=(0, 1)) + if np.ptp(avg_rgb) > 30: + is_color = True + except Exception as e: + print(f"Page {i+1} ink/color calc failed: {e}") + ink_pct = None + + if is_color: + rate = get_rate_color() + total_area_color += area + total_cost_color += area * rate + else: + rate = get_rate_black() + total_area_black += area + total_cost_black += area * rate + + pages_info.append({ + "page": i + 1, + "width_m": width_m, + "height_m": height_m, + "area_m2": area, + "ink_pct": ink_pct, + "is_color": is_color, + "cost": round(area * rate, 2) + }) + + return { + "filename": os.path.basename(path), + "pages": pages_info, + "total_area_black": total_area_black, + "total_area_color": total_area_color, + "total_cost_black": round(total_cost_black, 2), + "total_cost_color": round(total_cost_color, 2), + "grand_total": round(total_cost_black + total_cost_color, 2) + } diff --git a/data/uploads/Nachweis Arbeitsverhältnis Socius.pdf b/data/uploads/Nachweis Arbeitsverhältnis Socius.pdf new file mode 100644 index 0000000..e7cb46e Binary files /dev/null and b/data/uploads/Nachweis Arbeitsverhältnis Socius.pdf differ diff --git a/data/uploads/Poster AD (A3).pdf b/data/uploads/Poster AD (A3).pdf new file mode 100644 index 0000000..c33c828 Binary files /dev/null and b/data/uploads/Poster AD (A3).pdf differ diff --git a/data/uploads/Screen AD.pdf b/data/uploads/Screen AD.pdf new file mode 100644 index 0000000..db2d89c Binary files /dev/null and b/data/uploads/Screen AD.pdf differ diff --git a/data/uploads/Socius_Arbeitsvertrag.pdf b/data/uploads/Socius_Arbeitsvertrag.pdf new file mode 100644 index 0000000..e7c2b78 Binary files /dev/null and b/data/uploads/Socius_Arbeitsvertrag.pdf differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0c5fee0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ + +services: + web: + build: . + container_name: cost-dashboard + ports: + - "8000:8000" + working_dir: /cost-assistant + volumes: + - .:/cost-assistant # live reload for code/templates/static + - ./data/uploads:/cost-assistant/data/uploads # persistent uploads + env_file: + - .env # RATE_PER_M2_BLACK and RATE_PER_M2_COLOR + command: python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload diff --git a/main.py b/main.py new file mode 100644 index 0000000..bbe279a --- /dev/null +++ b/main.py @@ -0,0 +1,62 @@ +# main.py +import os +import shutil +from fastapi import FastAPI, UploadFile, File, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles + +from cost_calculator import allowed_file, analyze_pdf, get_rate_black, get_rate_color, UPLOAD_FOLDER + +app = FastAPI() +templates = Jinja2Templates(directory="templates") +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +app.mount("/static", StaticFiles(directory="static"), name="static") + + +@app.get("/", response_class=HTMLResponse) +async def welcome(request: Request): + return templates.TemplateResponse("landing.html", {"request": request}) + + +@app.get("/about", response_class=HTMLResponse) +async def about(request: Request): + return templates.TemplateResponse("about.html", {"request": request}) + + +@app.get("/cost", response_class=HTMLResponse) +async def cost_dashboard(request: Request): + return templates.TemplateResponse( + "cost-calculator.html", + { + "request": request, + "rate_black": get_rate_black(), + "rate_color": get_rate_color() + } + ) + + +@app.post("/upload") +async def upload_file(request: Request, file: UploadFile = File(...)): + if not allowed_file(file.filename): + return templates.TemplateResponse( + "cost-calculator.html", + {"request": request, "error": "Unsupported file type. Only PDF allowed."}, + ) + + path = os.path.join(UPLOAD_FOLDER, file.filename) + with open(path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + result = analyze_pdf(path) + + return templates.TemplateResponse( + "result.html", + { + "request": request, + "result": result, + "rate_black": get_rate_black(), + "rate_color": get_rate_color() + }, + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0d11f90 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +fastapi +uvicorn[standard] +jinja2 +python-multipart +pypdf2 +pdf2image +Pillow +numpy \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..881eb3c --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,98 @@ +@font-face { + font-family: "SISTEMAS"; + src: url("/static/fonts/SISTEMAS FONT BT.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "BauPro"; + src: url("/static/fonts/BauPro.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "SISTEMAS", sans-serif; +} + +body, p, input, button, label, a { + font-family: "BauPro", sans-serif; +} + + +body { + background-color: #F9F9F9; + color: #333333; + margin: 0; + padding: 0; +} + +header { + background-color: #001F4D; /* navy blue */ + color: #FFD600; /* yellow text */ + padding: 2em; + text-align: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +h1, h2, h3, h4, h5, h6 { + font-family: "SISTEMAS", sans-serif; + color: #001F4D; /* navy blue */ +} + +a.link-card { + display: block; + padding: 1.2em 1em; + background-color: #E6007E; /* magenta */ + color: #FFD600; /* yellow text */ + text-decoration: none; + border-radius: 10px; + text-align: center; + transition: all 0.2s ease-in-out; +} + +a.link-card:hover { + background-color: #FFD600; /* yellow hover */ + color: #001F4D; /* navy text on hover */ + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +button { + padding: 0.7em; + background-color: #E6007E; /* magenta */ + color: #FFD600; /* yellow text */ + border: none; + border-radius: 6px; + font-size: 1em; + cursor: pointer; +} + +button:hover { + background-color: #FFD600; /* yellow hover */ + color: #001F4D; /* navy text */ +} + +header { + display: flex; + align-items: center; + gap: 1em; + padding: 1em; + background-color: #001f3f; /* navy */ + color: #fff; +} + +.logo { + height: 60px; + cursor: pointer; +} + +nav a { + color: #fff; + text-decoration: none; + margin-right: 1em; +} + +nav a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/static/fonts/BauPro.ttf b/static/fonts/BauPro.ttf new file mode 100644 index 0000000..94b4ff8 Binary files /dev/null and b/static/fonts/BauPro.ttf differ diff --git a/static/fonts/SISTEMAS FONT BT.ttf b/static/fonts/SISTEMAS FONT BT.ttf new file mode 100644 index 0000000..08872e8 Binary files /dev/null and b/static/fonts/SISTEMAS FONT BT.ttf differ diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..ded3ed8 Binary files /dev/null and b/static/images/logo.png differ diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..4438baa --- /dev/null +++ b/templates/about.html @@ -0,0 +1,103 @@ + + + + + + Über uns – Studio EinsZwoVier + + + + +
+

Studio EinsZwoVier Makerspace

+

Am Gabriele-von-Bülow-Gymnasium

+
+ +
+
+

Über den Maker Space

+

Seit Dezember 2024 trägt unser Maker Space den Namen studio einszwovier. Er ist ein innovativer, digitaler Lernraum, der Kreativität, Technik und Bildungsgerechtigkeit verbindet. Hier wird „Making“ erlebbar: Lernende gestalten ihren Lernprozess aktiv, entdecken individuelle Stärken und erleben durch Selbstwirksamkeit besondere Motivation.

+
+ +
+

Ausstattung

+
    +
  • 3D-Drucker: Für Modelle und Prototypen.
  • +
  • Lasercutter: Präzises Schneiden und Gravieren von Materialien.
  • +
  • Microcontroller: Elektronik und Programmierung.
  • +
  • Holzbearbeitung: Handwerkliche Projekte.
  • +
  • Textildruckgeräte: Kreative Designs auf Stoffen.
  • +
+
+ +
+

Betreuungsteam

+

Betreut wird der Maker Space von Aron Petau und Friedrich Weber. Sie sind montags bis mittwochs von 11:00 bis 15:00 Uhr vor Ort. Einfach vorbeischauen, Ideen vorstellen und loslegen!

+
+ +
+

Öffnungszeiten

+

Dienstag bis Donnerstag: 11:00 – 16:00 Uhr
+ Raum 124, Gabriele-von-Bülow-Gymnasium

+
+ +
+

Kontakt

+

E-Mail: einszwovier@gvb-gymnasium.de

+
+ +
+

Aktuelle Kurse

+
    +
  • Löten und Leuchten: Herstellung von Nachttischleuchten mit 3D-Design und Löttechnik.
  • +
  • Die Vogelvilla: Bau von Vogelhäusern mit Lasercutter und Holzbearbeitung.
  • +
+
+ +
+

Standort

+

Gabriele-von-Bülow-Gymnasium
+ Tile-Brügge-Weg 63, 13509 Berlin (Tegel)
+ Telefon: 030 21 00 52 460
+ E-Mail: info@gvb-gymnasium.de

+
+
+ + + + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..3e88006 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,24 @@ + + + + + {% block title %}Studio Einszwovier{% endblock %} + + + +
+ + + + +
+ +
+ {% block content %}{% endblock %} +
+ + diff --git a/templates/cost-calculator.html b/templates/cost-calculator.html new file mode 100644 index 0000000..548809e --- /dev/null +++ b/templates/cost-calculator.html @@ -0,0 +1,81 @@ + + + + + + Print Cost Calculator + + + + +
+

Print Cost Calculator

+ + {% if error %} +

{{ error }}

+ {% endif %} + +
+ + +
+ +

+ Rates are fixed via environment variables:
+ B&W: {{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} € / m², + Color: {{ rate_color if rate_color else 'RATE_PER_M2_COLOR' }} € / m² +

+
+ + diff --git a/templates/landing.html b/templates/landing.html new file mode 100644 index 0000000..16c889e --- /dev/null +++ b/templates/landing.html @@ -0,0 +1,122 @@ + + + + + + + Studio Einszwovier + + + + + +
+ + Studio Einszwovier Logo + +

Studio Einszwovier

+

Welcome to the school makerspace portal at the GvB

+
+ +
+ About us + Print Cost Calculator + Contact: einszwovier@gvb-gymnasium.de + Log in to iServ + School website +
+ + + + + \ No newline at end of file diff --git a/templates/result.html b/templates/result.html new file mode 100644 index 0000000..533943e --- /dev/null +++ b/templates/result.html @@ -0,0 +1,74 @@ + + + + + + Print Cost Result + + + + +
+

Print Cost for {{ result.filename }}

+ + + + + + + + + + + + + + + {% for page in result.pages %} + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + +
PageWidth (m)Height (m)Area (m²)Ink %TypeCost (€)
{{ page.page }}{{ "%.3f"|format(page.width_m) }}{{ "%.3f"|format(page.height_m) }}{{ "%.4f"|format(page.area_m2) }}{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}{{ 'Color' if page.is_color else 'B&W' }}{{ "%.2f"|format(page.cost) }}
Total Black Pages{{ "%.4f"|format(result.total_area_black) }}{{ "%.2f"|format(result.total_cost_black) }}
Total Color Pages{{ "%.4f"|format(result.total_area_color) }}{{ "%.2f"|format(result.total_cost_color) }}
Grand Total{{ "%.2f"|format(result.grand_total) }}
+ +

+ Upload another PDF +

+
+ +