File size: 4,608 Bytes
93b1c3a
 
 
 
 
 
f1f487c
93b1c3a
 
 
f1f487c
 
 
 
 
 
93b1c3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1f487c
 
93b1c3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""
pdf_export.py — Build an illustrated PDF storybook with reportlab.
Returns the path to a temporary PDF file.
"""
import io
import os
import re
import tempfile


def _slugify(text: str) -> str:
    text = re.sub(r"[^\w\s-]", "", text.lower())
    text = re.sub(r"[\s_-]+", "_", text).strip("_")
    return text[:40]


def build_pdf(
    theme: str,
    hero: str,
    world: str,
    beats: list[str],
    images: list[bytes],
) -> str:
    """
    Lay out beats + images as an A5 picture book.
    Returns path to a temporary PDF file (caller is responsible for cleanup).
    """
    from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import A5
    from reportlab.lib.styles import ParagraphStyle
    from reportlab.lib.units import cm
    from reportlab.platypus import (
        HRFlowable,
        Image as RLImage,
        Paragraph,
        SimpleDocTemplate,
        Spacer,
    )

    slug = _slugify(f"{theme} {hero}") or "storyforge"
    tmp = tempfile.NamedTemporaryFile(prefix=f"{slug}_", suffix=".pdf", delete=False)
    tmp.close()

    W, H = A5
    margin = 1.8 * cm
    body_w = W - 2 * margin

    doc = SimpleDocTemplate(
        tmp.name,
        pagesize=A5,
        leftMargin=margin,
        rightMargin=margin,
        topMargin=2.0 * cm,
        bottomMargin=2.0 * cm,
    )

    gold = colors.HexColor("#d4a843")
    rust = colors.HexColor("#c85a33")
    tan  = colors.HexColor("#ecdbb8")
    ink  = colors.HexColor("#2c1810")

    h1 = ParagraphStyle("H1", fontName="Helvetica-Bold", fontSize=20,
                         alignment=TA_CENTER, spaceAfter=4, leading=26, textColor=rust)
    sub = ParagraphStyle("Sub", fontName="Helvetica", fontSize=10,
                          alignment=TA_CENTER, spaceAfter=3, textColor=colors.HexColor("#5a3e2b"))
    lbl = ParagraphStyle("Lbl", fontName="Helvetica-Bold", fontSize=8,
                          alignment=TA_CENTER, textColor=rust, spaceAfter=4)
    body = ParagraphStyle("Body", fontName="Helvetica", fontSize=11,
                           alignment=TA_JUSTIFY, leading=17, spaceAfter=6, textColor=ink)
    end_style = ParagraphStyle("End", fontName="Helvetica-Bold", fontSize=18,
                                alignment=TA_CENTER, textColor=colors.HexColor("#4a8c5c"))

    story = []

    # ── Cover ─────────────────────────────────────────────────────────────────
    story.append(Spacer(1, 1.2 * cm))
    story.append(Paragraph("📖 StoryForge", h1))
    story.append(Paragraph(theme, sub))
    if hero:
        story.append(Paragraph(f"Hero: {hero}", sub))
    if world:
        story.append(Paragraph(f"World: {world}", sub))
    if images:
        story.append(Spacer(1, 0.4 * cm))
        _img(story, images[0], body_w, 8 * cm)
    story.append(Spacer(1, 0.8 * cm))
    story.append(HRFlowable(width="80%", color=gold, thickness=1.5))
    story.append(Spacer(1, 0.8 * cm))

    # ── Beats ─────────────────────────────────────────────────────────────────
    for i, beat_text in enumerate(beats):
        story.append(Paragraph(f"— Moment {i + 1} of {len(beats)} —", lbl))
        img_bytes = images[i] if i < len(images) else None
        if img_bytes:
            _img(story, img_bytes, body_w, 6 * cm)
            story.append(Spacer(1, 0.25 * cm))
        story.append(Paragraph(beat_text, body))
        story.append(Spacer(1, 0.5 * cm))
        story.append(HRFlowable(width="50%", color=tan, thickness=1))
        story.append(Spacer(1, 0.5 * cm))

    # ── The End ───────────────────────────────────────────────────────────────
    story.append(Paragraph("&#10024; The End &#10024;", end_style))

    doc.build(story)
    return tmp.name


def _img(story, img_bytes: bytes, max_w: float, max_h: float):
    from PIL import Image as PILImage
    from reportlab.platypus import Image as RLImage

    pil = PILImage.open(io.BytesIO(img_bytes)).convert("RGB")
    w_px, h_px = pil.size
    aspect = h_px / w_px
    w = max_w
    h = w * aspect
    if h > max_h:
        h = max_h
        w = h / aspect
    buf = io.BytesIO()
    pil.save(buf, format="PNG")
    buf.seek(0)
    story.append(RLImage(buf, width=w, height=h))