chore: initial import
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
recipe_builder.py
|
||||
Rendert eine Rezeptsammlung als A4-PDF im Design der Original-Editionen:
|
||||
farbiges Kategorie-Band, farbiges Rezept-Titelband (weiße Schrift), getönter
|
||||
Untertitel, NÄHRWERTE-Strip, zweispaltig ZUTATEN | SO GEHT'S, getönte
|
||||
VARIATIONEN- und Tipp-Box, dünn umrandete Karte.
|
||||
|
||||
Regeln (wie im Original-Generator):
|
||||
* jedes Rezept ist KeepTogether -> wird nie durch einen Seitenumbruch geteilt
|
||||
* vor jeder Kategorie ein PageBreak -> jede Kategorie beginnt auf neuer Seite
|
||||
|
||||
build_pdf(cfg) -> bytes ; default_config() -> dict
|
||||
"""
|
||||
import re
|
||||
import copy
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
KeepTogether, PageBreak)
|
||||
|
||||
DEFAULTS = {
|
||||
"type": "recipe",
|
||||
"meta": {
|
||||
"title": "Neue Rezeptsammlung",
|
||||
"header_left": "REZEPTE · Edition",
|
||||
"header_right": "Begleitend zum 8-Wochen-Plan",
|
||||
"footer_left": "Teller = Portionsgrenze · Trend über die Woche zählt",
|
||||
"output_name": "Rezepte.pdf",
|
||||
"doc_title": "Rezeptsammlung",
|
||||
"brand_color": "#6D28D9",
|
||||
"tip_label": "Tipp",
|
||||
"steps_label": "SO GEHT'S",
|
||||
},
|
||||
"intro": {
|
||||
"headline": "Neue Rezeptsammlung",
|
||||
"subtitle": "Kurzbeschreibung der Edition.",
|
||||
"paragraphs": ["Einleitender Absatz."],
|
||||
"rule_head": "Die eine Grundregel",
|
||||
"rule_text": "Teller fertig machen, Rest aus dem Sichtfeld.",
|
||||
"categories_head": "1 Kategorie",
|
||||
},
|
||||
"categories": [{"name": "Cremig & süß", "color": "#7C3AED"}],
|
||||
"recipes": [
|
||||
{"num": "01", "category": "Cremig & süß", "title": "Beispiel-Rezept",
|
||||
"tagline": "Kurzer Untertitel.", "nutrition": "", "badge": "", "yield": "",
|
||||
"ingredients": "200 g Magerquark · etwas Süße",
|
||||
"steps": "Alles verrühren, fertig.",
|
||||
"variations": ["Schoko: + 1 TL Kakao"], "tip": "Tipp Eiweiß-reich."},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def default_config():
|
||||
return copy.deepcopy(DEFAULTS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- helpers
|
||||
_ENT = re.compile(r"&(?!(?:amp|lt|gt|quot|apos|nbsp|#\d+|#x[0-9a-fA-F]+);)")
|
||||
|
||||
|
||||
def esc(s):
|
||||
if s is None:
|
||||
return ""
|
||||
s = str(s).replace("<", "<").replace(">", ">")
|
||||
return _ENT.sub("&", s)
|
||||
|
||||
|
||||
def _hx(c):
|
||||
return "#" + c.hexval()[2:]
|
||||
|
||||
|
||||
def lighten(hexstr, w=0.92):
|
||||
"""Mischt eine Farbe mit Weiß (w = Weiß-Anteil) -> heller Ton."""
|
||||
try:
|
||||
c = colors.HexColor(hexstr)
|
||||
except Exception:
|
||||
c = colors.HexColor("#334155")
|
||||
r = c.red * 255 * (1 - w) + 255 * w
|
||||
g = c.green * 255 * (1 - w) + 255 * w
|
||||
b = c.blue * 255 * (1 - w) + 255 * w
|
||||
return colors.Color(r / 255, g / 255, b / 255)
|
||||
|
||||
|
||||
TIP_LABEL_RE = re.compile(r"^\s*([A-Za-zÄÖÜäöü]+-Tipp|Tipp)\b[:\s]*(.*)$", re.S)
|
||||
|
||||
|
||||
def split_tip(text, default_label):
|
||||
"""Zerlegt 'Morgen-Tipp Text' -> ('Morgen-Tipp', 'Text')."""
|
||||
if not text:
|
||||
return default_label, ""
|
||||
m = TIP_LABEL_RE.match(text)
|
||||
if m:
|
||||
return m.group(1), m.group(2).strip()
|
||||
return default_label, text.strip()
|
||||
|
||||
|
||||
def bold_lead(v):
|
||||
"""Erste Bezeichnung einer Variation fett (bis Doppelpunkt bzw. erstes Wort)."""
|
||||
v = v.strip()
|
||||
m = re.match(r"^([^:]{2,40}):\s*(.*)$", v, re.S)
|
||||
if m:
|
||||
return "<b>%s:</b> %s" % (esc(m.group(1)), esc(m.group(2)))
|
||||
m = re.match(r"^(\S+)\s+(.*)$", v, re.S)
|
||||
if m:
|
||||
return "<b>%s</b> %s" % (esc(m.group(1)), esc(m.group(2)))
|
||||
return esc(v)
|
||||
|
||||
|
||||
NOSPLIT = {"bis", "damit", "sodass", "bevor", "nachdem", "während", "wenn", "falls", "sobald",
|
||||
"weil", "sowie", "oder", "aber", "bei", "je", "ohne", "mit", "in", "im", "am", "zum",
|
||||
"zur", "auf", "für", "über", "unter", "dabei", "dazu", "nach", "vor", "als"}
|
||||
|
||||
|
||||
def split_steps(text):
|
||||
t = (text or "").strip().replace("z. B.", "z.B.")
|
||||
raw = re.split(r"(?<=[.!?])\s+|,\s+", t)
|
||||
steps = []
|
||||
for frag in raw:
|
||||
frag = frag.strip()
|
||||
if not frag:
|
||||
continue
|
||||
first = frag.split()[0].lower().strip("(„\"'.:;»«")
|
||||
if steps and first in NOSPLIT:
|
||||
steps[-1] = steps[-1].rstrip(".") + ", " + frag
|
||||
else:
|
||||
steps.append(frag)
|
||||
steps = [s if s.endswith((".", "!", "?")) else s + "." for s in steps]
|
||||
return [s[:1].upper() + s[1:] for s in steps]
|
||||
|
||||
|
||||
def numbered_steps(steps_txt):
|
||||
"""Bereits nummerierte (mehrzeilige) Schritte -> Zeilen; sonst Prosa splitten."""
|
||||
if steps_txt and "\n" in steps_txt:
|
||||
out = []
|
||||
for ln in steps_txt.split("\n"):
|
||||
ln = re.sub(r"^\s*\d+[\.\)]?\s+", "", ln.strip())
|
||||
if ln:
|
||||
out.append(ln)
|
||||
return out
|
||||
return split_steps(steps_txt)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- renderer
|
||||
def build_pdf(cfg):
|
||||
_D = DEFAULTS
|
||||
meta = dict(_D["meta"]); meta.update(cfg.get("meta") or {})
|
||||
intro = dict(_D["intro"]); intro.update(cfg.get("intro") or {})
|
||||
cats = cfg.get("categories") or _D["categories"]
|
||||
recipes = cfg.get("recipes") or []
|
||||
|
||||
INK = colors.HexColor("#23272F")
|
||||
SUB = colors.HexColor("#5B6270")
|
||||
HAIR = colors.HexColor("#E3E1EA")
|
||||
NUTBG = colors.HexColor("#F7F7F9")
|
||||
BRAND = colors.HexColor(meta.get("brand_color", "#6D28D9"))
|
||||
TIPBG = lighten(_hx(BRAND), 0.93)
|
||||
|
||||
def catcolor(name):
|
||||
for c in cats:
|
||||
if c.get("name") == name:
|
||||
try:
|
||||
return colors.HexColor(c.get("color", "#334155"))
|
||||
except Exception:
|
||||
return colors.HexColor("#334155")
|
||||
return colors.HexColor("#334155")
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
def S(name, **kw):
|
||||
base = kw.pop("parent", styles["Normal"])
|
||||
return ParagraphStyle(name, parent=base, **kw)
|
||||
|
||||
st_title = S("t", fontName="Helvetica-Bold", fontSize=22, leading=25, textColor=INK)
|
||||
st_sub = S("s", fontName="Helvetica", fontSize=10.5, leading=14, textColor=SUB)
|
||||
st_intro = S("i", fontName="Helvetica", fontSize=10, leading=15, textColor=INK)
|
||||
st_callh = S("ch", fontName="Helvetica-Bold", fontSize=10.5, leading=13, textColor=INK)
|
||||
st_legend= S("lg", fontName="Helvetica", fontSize=8.6, leading=12, textColor=INK)
|
||||
st_cat = S("c", fontName="Helvetica-Bold", fontSize=12.5, leading=15, textColor=colors.white)
|
||||
st_rname = S("rn", fontName="Helvetica-Bold", fontSize=11.5, leading=14, textColor=colors.white)
|
||||
st_badge = S("bd", fontName="Helvetica-Bold", fontSize=7.5, leading=9)
|
||||
st_tag = S("tg", fontName="Helvetica-Oblique", fontSize=9, leading=12, textColor=SUB)
|
||||
st_lbl = S("lb", fontName="Helvetica-Bold", fontSize=7.5, leading=10, spaceAfter=1)
|
||||
st_body = S("bd2", fontName="Helvetica", fontSize=8.8, leading=12, textColor=INK)
|
||||
st_var = S("vr", fontName="Helvetica", fontSize=8.4, leading=11.6, textColor=INK)
|
||||
st_tip = S("tp", fontName="Helvetica", fontSize=8.5, leading=12, textColor=INK)
|
||||
st_yield = S("yl", fontName="Helvetica-Oblique", fontSize=8, leading=11, textColor=SUB)
|
||||
|
||||
PAGE_W, PAGE_H = A4
|
||||
LM = RM = 1.5 * cm
|
||||
CW = PAGE_W - LM - RM
|
||||
|
||||
def lbl(text, acc):
|
||||
return Paragraph(text, S("lx", parent=st_lbl, textColor=acc))
|
||||
|
||||
# ---- category band ----
|
||||
def category_band(name):
|
||||
acc = catcolor(name)
|
||||
t = Table([[Paragraph(esc(name), st_cat)]], colWidths=[CW])
|
||||
t.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6)]))
|
||||
return t
|
||||
|
||||
# ---- one recipe card ----
|
||||
def recipe_card(r):
|
||||
acc = catcolor(r.get("category", ""))
|
||||
accx = _hx(acc)
|
||||
tint = lighten(accx, 0.92)
|
||||
|
||||
# Titelband (+ optional AIRFRYER-Badge)
|
||||
name_p = Paragraph("%s %s" % (esc(r.get("num", "")), esc(r.get("title", ""))), st_rname)
|
||||
if r.get("badge"):
|
||||
badge = Table([[Paragraph(esc(r["badge"]), S("bx", parent=st_badge, textColor=acc))]])
|
||||
badge.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), colors.white),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 5), ("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2)]))
|
||||
badge.hAlign = "RIGHT"
|
||||
head = Table([[name_p, badge]], colWidths=[CW - 82, 82])
|
||||
head.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (0, 0), 9), ("RIGHTPADDING", (0, 0), (0, 0), 4),
|
||||
("LEFTPADDING", (1, 0), (1, 0), 0), ("RIGHTPADDING", (1, 0), (1, 0), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)]))
|
||||
else:
|
||||
head = Table([[name_p]], colWidths=[CW])
|
||||
head.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)]))
|
||||
|
||||
rows = [[head]]
|
||||
|
||||
# Untertitel (getönt)
|
||||
if r.get("tagline"):
|
||||
tg = Table([[Paragraph(esc(r["tagline"]), st_tag)]], colWidths=[CW])
|
||||
tg.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), tint),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4)]))
|
||||
rows.append([tg])
|
||||
|
||||
# NÄHRWERTE-Strip
|
||||
if r.get("nutrition"):
|
||||
ntext = '<b><font color="%s">NÄHRWERTE</font></b> %s' % (accx, esc(r["nutrition"]))
|
||||
n = Table([[Paragraph(ntext, st_var)]], colWidths=[CW])
|
||||
n.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), NUTBG),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||
("LINEBELOW", (0, 0), (-1, 0), 0.6, HAIR)]))
|
||||
rows.append([n])
|
||||
|
||||
# ZUTATEN – volle Breite
|
||||
zut_cell = [lbl("ZUTATEN", acc), Paragraph(esc(r.get("ingredients", "")), st_body)]
|
||||
if r.get("yield"):
|
||||
zut_cell.append(Spacer(1, 2))
|
||||
zut_cell.append(Paragraph("<i>%s</i>" % esc(r["yield"]), st_yield))
|
||||
zut = Table([[zut_cell]], colWidths=[CW])
|
||||
zut.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
||||
("BACKGROUND", (0, 0), (-1, -1), colors.white)]))
|
||||
rows.append([zut])
|
||||
|
||||
# Schritte – nummeriert, volle Breite
|
||||
steps = numbered_steps(r.get("steps", ""))
|
||||
srows = [[Paragraph('<b><font color="%s">%d</font></b>' % (accx, i), st_body),
|
||||
Paragraph(esc(s), st_body)] for i, s in enumerate(steps, 1)]
|
||||
if srows:
|
||||
steps_inner = Table(srows, colWidths=[15, CW - 18 - 15])
|
||||
steps_inner.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (0, 0), 4), ("RIGHTPADDING", (1, 0), (1, 0), 0),
|
||||
("TOPPADDING", (0, 0), (-1, 0), 0), ("TOPPADDING", (0, 1), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 0)]))
|
||||
arb_cell = [lbl(meta.get("steps_label", "SO GEHT'S"), acc), Spacer(1, 2), steps_inner]
|
||||
else:
|
||||
arb_cell = [lbl(meta.get("steps_label", "SO GEHT'S"), acc)]
|
||||
arb = Table([[arb_cell]], colWidths=[CW])
|
||||
arb.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
|
||||
("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR), ("BACKGROUND", (0, 0), (-1, -1), colors.white)]))
|
||||
rows.append([arb])
|
||||
|
||||
# ZUBEREITUNG (Airfryer/Ofen), optional
|
||||
if r.get("methods"):
|
||||
m_cell = [lbl("ZUBEREITUNG · AIRFRYER / OFEN", acc), Paragraph(esc(r["methods"]), st_var)]
|
||||
mt = Table([[m_cell]], colWidths=[CW])
|
||||
mt.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#F3F4F6")),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
||||
("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR)]))
|
||||
rows.append([mt])
|
||||
|
||||
# VARIATIONEN (getönt)
|
||||
if r.get("variations"):
|
||||
vhtml = "<br/>".join(bold_lead(v) for v in r["variations"] if v)
|
||||
v = Table([[[lbl("VARIATIONEN", acc), Paragraph(vhtml, st_var)]]], colWidths=[CW])
|
||||
v.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), tint),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
||||
("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR)]))
|
||||
rows.append([v])
|
||||
|
||||
# Tipp (Marken-Tint)
|
||||
if r.get("tip"):
|
||||
lab, txt = split_tip(r["tip"], meta.get("tip_label", "Tipp"))
|
||||
tip_html = '<b><font color="%s">%s</font></b> %s' % (accx, esc(lab), esc(txt))
|
||||
tp = Table([[Paragraph(tip_html, st_tip)]], colWidths=[CW])
|
||||
tp.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), TIPBG),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)]))
|
||||
rows.append([tp])
|
||||
|
||||
card = Table(rows, colWidths=[CW])
|
||||
card.setStyle(TableStyle([("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
|
||||
("BOX", (0, 0), (-1, -1), 0.8, HAIR)]))
|
||||
return KeepTogether([card, Spacer(1, 7)])
|
||||
|
||||
# ---- story ----
|
||||
story = []
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
story.append(Paragraph(esc(intro.get("headline", meta["title"])), st_title))
|
||||
story.append(Spacer(1, 3))
|
||||
if intro.get("subtitle"):
|
||||
story.append(Paragraph(esc(intro["subtitle"]), st_sub))
|
||||
story.append(Spacer(1, 10))
|
||||
for p in intro.get("paragraphs", []):
|
||||
story.append(Paragraph(esc(p), st_intro))
|
||||
story.append(Spacer(1, 6))
|
||||
if intro.get("rule_head") or intro.get("rule_text"):
|
||||
gr = Table([[[Paragraph(esc(intro.get("rule_head", "")), st_callh), Spacer(1, 3),
|
||||
Paragraph(esc(intro.get("rule_text", "")), st_intro)]]], colWidths=[CW])
|
||||
gr.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), TIPBG),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 12), ("RIGHTPADDING", (0, 0), (-1, -1), 12),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 9), ("BOTTOMPADDING", (0, 0), (-1, -1), 9),
|
||||
("LINEBEFORE", (0, 0), (0, 0), 3, BRAND)]))
|
||||
story.append(Spacer(1, 4))
|
||||
story.append(gr)
|
||||
story.append(Spacer(1, 12))
|
||||
story.append(Paragraph(esc(intro.get("categories_head", "%d Kategorien" % len(cats))),
|
||||
S("lh", parent=st_callh, fontSize=10)))
|
||||
story.append(Spacer(1, 4))
|
||||
leg_rows = []
|
||||
for c in cats:
|
||||
sw = Table([[""]], colWidths=[10], rowHeights=[10])
|
||||
sw.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), catcolor(c["name"])),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0)]))
|
||||
leg_rows.append([sw, Paragraph(esc(c["name"]), st_legend)])
|
||||
if leg_rows:
|
||||
leg = Table(leg_rows, colWidths=[16, CW - 16])
|
||||
leg.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (0, 0), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2)]))
|
||||
story.append(leg)
|
||||
|
||||
# Rezepte, gruppiert je Kategorie -> neue Seite pro Kategorie, Rezept nie geteilt
|
||||
cur = None
|
||||
for r in recipes:
|
||||
c = r.get("category", "")
|
||||
if c != cur:
|
||||
story.append(PageBreak())
|
||||
story.append(category_band(c))
|
||||
story.append(Spacer(1, 7))
|
||||
cur = c
|
||||
story.append(recipe_card(r))
|
||||
|
||||
# ---- Kopf-/Fußzeile ----
|
||||
header_left = meta.get("header_left", "")
|
||||
header_right = meta.get("header_right", "Begleitend zum 8-Wochen-Plan")
|
||||
footer_left = meta.get("footer_left", "")
|
||||
|
||||
def deco(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFillColor(BRAND); canvas.setFont("Helvetica-Bold", 8)
|
||||
canvas.drawString(LM, PAGE_H - 1.05 * cm, header_left)
|
||||
canvas.setFont("Helvetica", 8); canvas.setFillColor(SUB)
|
||||
canvas.drawRightString(PAGE_W - RM, PAGE_H - 1.05 * cm, header_right)
|
||||
canvas.setStrokeColor(HAIR); canvas.setLineWidth(0.7)
|
||||
canvas.line(LM, PAGE_H - 1.2 * cm, PAGE_W - RM, PAGE_H - 1.2 * cm)
|
||||
canvas.line(LM, 1.15 * cm, PAGE_W - RM, 1.15 * cm)
|
||||
canvas.setFont("Helvetica", 7.5); canvas.setFillColor(SUB)
|
||||
canvas.drawString(LM, 0.8 * cm, footer_left)
|
||||
canvas.drawRightString(PAGE_W - RM, 0.8 * cm, "Seite %d" % doc.page)
|
||||
canvas.restoreState()
|
||||
|
||||
buf = BytesIO()
|
||||
doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=LM, rightMargin=RM,
|
||||
topMargin=1.6 * cm, bottomMargin=1.5 * cm,
|
||||
title=meta.get("doc_title", meta.get("title", "Rezepte")))
|
||||
doc.build(story, onFirstPage=deco, onLaterPages=deco)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
open("/tmp/recipe_test.pdf", "wb").write(build_pdf(default_config()))
|
||||
print("OK")
|
||||
Reference in New Issue
Block a user