Spaces:
Sleeping
Sleeping
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("✨ The End ✨", 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))
|