Codex Claude Sonnet 4.6 commited on
Commit ·
620b7e6
1
Parent(s): 786a8ec
PDF cover: cream card layout matching in-app HTML cover
Browse filesReplace full-bleed approach with cream paper background + framed FLUX
illustration (sun inner border, ink outer border) + title below + badge —
identical visual language to the HTML cover shown in the app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- book_builder.py +48 -47
book_builder.py
CHANGED
|
@@ -226,70 +226,71 @@ def render_cover_image(
|
|
| 226 |
Falls back to cream-background design when no illustration is available.
|
| 227 |
"""
|
| 228 |
W, H = 1240, 1754
|
| 229 |
-
sun
|
| 230 |
-
leaf
|
| 231 |
-
white
|
| 232 |
-
ink
|
| 233 |
-
berry
|
| 234 |
-
dark = (30, 26, 24)
|
| 235 |
|
| 236 |
if first_page_bytes:
|
| 237 |
-
# ---
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
src_r = src.width / src.height
|
| 241 |
-
tar_r = W / H
|
| 242 |
-
if src_r > tar_r:
|
| 243 |
-
new_h, new_w = H, int(H * src_r)
|
| 244 |
-
else:
|
| 245 |
-
new_w, new_h = W, int(W / src_r)
|
| 246 |
-
src = src.resize((new_w, new_h), Image.LANCZOS)
|
| 247 |
-
left = (new_w - W) // 2
|
| 248 |
-
top = (new_h - H) // 2
|
| 249 |
-
base = src.crop((left, top, left + W, top + H)).convert("RGBA")
|
| 250 |
-
|
| 251 |
-
# Dark banner overlays (semi-transparent)
|
| 252 |
-
ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
| 253 |
-
dov = ImageDraw.Draw(ov)
|
| 254 |
-
# Top strip (kicker)
|
| 255 |
-
dov.rectangle([0, 0, W, 170], fill=(*dark, 210))
|
| 256 |
-
# Bottom strip (title + badge) — taller when title is long
|
| 257 |
-
dov.rectangle([0, H - 540, W, H], fill=(*dark, 215))
|
| 258 |
-
# Decorative inner border
|
| 259 |
-
for i in range(3):
|
| 260 |
-
dov.rectangle([i*10, i*10, W - i*10, H - i*10],
|
| 261 |
-
outline=(255, 255, 255, 60), width=2)
|
| 262 |
-
|
| 263 |
-
img = Image.alpha_composite(base, ov).convert("RGB")
|
| 264 |
draw = ImageDraw.Draw(img)
|
|
|
|
| 265 |
|
| 266 |
# Kicker
|
| 267 |
-
font_kicker = _load_font(FONT_CAVEAT,
|
| 268 |
-
draw.text((W // 2,
|
| 269 |
-
font=font_kicker, fill=
|
| 270 |
-
|
| 271 |
-
#
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
for i, line in enumerate(lines):
|
| 278 |
cy = y0 + i * line_h
|
| 279 |
draw.text((W // 2 + 3, cy + 3), line, font=font_title,
|
| 280 |
fill=sun, anchor="mm")
|
| 281 |
draw.text((W // 2, cy), line, font=font_title,
|
| 282 |
-
fill=
|
|
|
|
| 283 |
|
| 284 |
# Badge
|
| 285 |
font_badge = _load_font(FONT_GAEGU, 46)
|
| 286 |
bb = font_badge.getbbox(badge_text)
|
| 287 |
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
| 288 |
bx = W // 2 - (tw + 44) // 2
|
| 289 |
-
by = H -
|
| 290 |
-
draw.rounded_rectangle([bx, by
|
| 291 |
radius=14, fill=leaf)
|
| 292 |
-
draw.text((W // 2, by
|
| 293 |
font=font_badge, fill=white, anchor="mm")
|
| 294 |
|
| 295 |
else:
|
|
|
|
| 226 |
Falls back to cream-background design when no illustration is available.
|
| 227 |
"""
|
| 228 |
W, H = 1240, 1754
|
| 229 |
+
sun = (244, 198, 74)
|
| 230 |
+
leaf = (116, 184, 90)
|
| 231 |
+
white = (255, 255, 255)
|
| 232 |
+
ink = (46, 42, 38)
|
| 233 |
+
berry = (214, 81, 122)
|
|
|
|
| 234 |
|
| 235 |
if first_page_bytes:
|
| 236 |
+
# --- Cream card cover: same layout as the in-app HTML cover ---
|
| 237 |
+
cream = (255, 248, 230)
|
| 238 |
+
img = Image.new("RGB", (W, H), cream)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
draw = ImageDraw.Draw(img)
|
| 240 |
+
_paper_grain(img, amount=5000)
|
| 241 |
|
| 242 |
# Kicker
|
| 243 |
+
font_kicker = _load_font(FONT_CAVEAT, 66)
|
| 244 |
+
draw.text((W // 2, 118), "✨ a DoodleBook story ✨",
|
| 245 |
+
font=font_kicker, fill=berry, anchor="mm")
|
| 246 |
+
|
| 247 |
+
# FLUX illustration — square-crop & resize to 900 px
|
| 248 |
+
src = Image.open(io.BytesIO(first_page_bytes)).convert("RGB")
|
| 249 |
+
min_d = min(src.width, src.height)
|
| 250 |
+
src = src.crop(((src.width - min_d) // 2, (src.height - min_d) // 2,
|
| 251 |
+
(src.width + min_d) // 2, (src.height + min_d) // 2))
|
| 252 |
+
src = src.resize((900, 900), Image.LANCZOS)
|
| 253 |
+
|
| 254 |
+
# Frame: sun inner (12 px) + ink outer (16 px), rounded corners
|
| 255 |
+
pad_inner, pad_outer = 12, 16
|
| 256 |
+
pad = pad_inner + pad_outer
|
| 257 |
+
ix, iy = (W - 900) // 2, 200 # top-left of image
|
| 258 |
+
# Ink outer border
|
| 259 |
+
draw.rounded_rectangle(
|
| 260 |
+
[ix - pad, iy - pad, ix + 900 + pad, iy + 900 + pad],
|
| 261 |
+
radius=30, fill=ink,
|
| 262 |
+
)
|
| 263 |
+
# Sun inner border
|
| 264 |
+
draw.rounded_rectangle(
|
| 265 |
+
[ix - pad_inner, iy - pad_inner, ix + 900 + pad_inner, iy + 900 + pad_inner],
|
| 266 |
+
radius=20, fill=sun,
|
| 267 |
+
)
|
| 268 |
+
# Paste illustration
|
| 269 |
+
img.paste(src, (ix, iy))
|
| 270 |
+
draw = ImageDraw.Draw(img)
|
| 271 |
+
|
| 272 |
+
# Title — below frame
|
| 273 |
+
font_title = _load_font(FONT_GAEGU, 100)
|
| 274 |
+
lines = _wrap_title(title, font_title, W - 160)
|
| 275 |
+
line_h = 118
|
| 276 |
+
y0 = iy + 900 + pad + 56
|
| 277 |
for i, line in enumerate(lines):
|
| 278 |
cy = y0 + i * line_h
|
| 279 |
draw.text((W // 2 + 3, cy + 3), line, font=font_title,
|
| 280 |
fill=sun, anchor="mm")
|
| 281 |
draw.text((W // 2, cy), line, font=font_title,
|
| 282 |
+
fill=ink, anchor="mm")
|
| 283 |
+
total_h = len(lines) * line_h
|
| 284 |
|
| 285 |
# Badge
|
| 286 |
font_badge = _load_font(FONT_GAEGU, 46)
|
| 287 |
bb = font_badge.getbbox(badge_text)
|
| 288 |
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
| 289 |
bx = W // 2 - (tw + 44) // 2
|
| 290 |
+
by = max(y0 + total_h + 60, H - 160)
|
| 291 |
+
draw.rounded_rectangle([bx, by, bx + tw + 44, by + th + 24],
|
| 292 |
radius=14, fill=leaf)
|
| 293 |
+
draw.text((W // 2, by + th // 2 + 12), badge_text,
|
| 294 |
font=font_badge, fill=white, anchor="mm")
|
| 295 |
|
| 296 |
else:
|