Codex Claude Sonnet 4.6 commited on
Commit
620b7e6
·
1 Parent(s): 786a8ec

PDF cover: cream card layout matching in-app HTML cover

Browse files

Replace 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>

Files changed (1) hide show
  1. 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 = (244, 198, 74)
230
- leaf = (116, 184, 90)
231
- white = (255, 255, 255)
232
- ink = (46, 42, 38)
233
- berry = (214, 81, 122)
234
- dark = (30, 26, 24)
235
 
236
  if first_page_bytes:
237
- # --- Full-bleed FLUX illustration cover ---
238
- src = Image.open(io.BytesIO(first_page_bytes)).convert("RGB")
239
- # Scale-and-crop to fill A4 exactly
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, 68)
268
- draw.text((W // 2, 88), "✨ a DoodleBook story ✨",
269
- font=font_kicker, fill=sun, anchor="mm")
270
-
271
- # Title
272
- font_title = _load_font(FONT_GAEGU, 108)
273
- lines = _wrap_title(title, font_title, W - 140)
274
- line_h = 128
275
- total_h = len(lines) * line_h
276
- y0 = H - 510 + max(0, (460 - total_h)) // 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=white, anchor="mm")
 
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 - 72
290
- draw.rounded_rectangle([bx, by - th - 16, bx + tw + 44, by],
291
  radius=14, fill=leaf)
292
- draw.text((W // 2, by - th // 2 - 8), badge_text,
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: