#!/usr/bin/env python3 """Generate a preview of the placeholder cover layout at full cover size (480x800). This mirrors the C++ PlaceholderCoverGenerator layout logic for visual verification. """ from PIL import Image, ImageDraw, ImageFont import sys import os # Reuse the book icon generator sys.path.insert(0, os.path.dirname(__file__)) from generate_book_icon import generate_book_icon def create_preview(width=480, height=800, title="The Great Gatsby", author="F. Scott Fitzgerald"): img = Image.new("1", (width, height), 1) # White draw = ImageDraw.Draw(img) # Proportional layout constants edge_padding = max(3, width // 48) # ~10px at 480w border_width = max(2, width // 96) # ~5px at 480w inner_padding = max(4, width // 32) # ~15px at 480w title_scale = 2 if height >= 600 else 1 author_scale = 2 if height >= 600 else 1 # Author also larger on full covers icon_scale = 2 if height >= 600 else (1 if height >= 350 else 0) # Draw border inset from edge bx = edge_padding by = edge_padding bw = width - 2 * edge_padding bh = height - 2 * edge_padding for i in range(border_width): draw.rectangle([bx + i, by + i, bx + bw - 1 - i, by + bh - 1 - i], outline=0) # Content area content_x = edge_padding + border_width + inner_padding content_y = edge_padding + border_width + inner_padding content_w = width - 2 * content_x content_h = height - 2 * content_y # Zones title_zone_h = content_h * 2 // 3 author_zone_h = content_h - title_zone_h author_zone_y = content_y + title_zone_h # Separator sep_w = content_w // 3 sep_x = content_x + (content_w - sep_w) // 2 draw.line([sep_x, author_zone_y, sep_x + sep_w, author_zone_y], fill=0) # Use a basic font for the preview (won't match exact Ubuntu metrics, but shows layout) try: title_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 12 * title_scale) author_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 10 * author_scale) except (OSError, IOError): title_font = ImageFont.load_default() author_font = ImageFont.load_default() # Icon dimensions (needed for title text wrapping) icon_w_px = 48 * icon_scale if icon_scale > 0 else 0 icon_h_px = 48 * icon_scale if icon_scale > 0 else 0 icon_gap = max(8, width // 40) if icon_scale > 0 else 0 title_text_w = content_w - icon_w_px - icon_gap # Title wraps in narrower area beside icon # Wrap title (within the narrower area to the right of the icon) title_lines = [] words = title.split() current_line = "" for word in words: test = f"{current_line} {word}".strip() bbox = draw.textbbox((0, 0), test, font=title_font) if bbox[2] - bbox[0] <= title_text_w: current_line = test else: if current_line: title_lines.append(current_line) current_line = word if current_line: title_lines.append(current_line) title_lines = title_lines[:5] # Line spacing: 75% of advanceY (tighter so 2-3 lines fit within icon height) title_line_h = 29 * title_scale * 3 // 4 # Based on C++ ubuntu_12_bold advanceY # Measure actual single-line height from the PIL font for accurate centering sample_bbox = draw.textbbox((0, 0), "Ag", font=title_font) # Tall + descender chars single_line_visual_h = sample_bbox[3] - sample_bbox[1] # Visual height: line spacing between lines + actual height of last line's glyphs num_title_lines = len(title_lines) title_visual_h = (num_title_lines - 1) * title_line_h + single_line_visual_h if num_title_lines > 0 else 0 title_block_h = max(icon_h_px, title_visual_h) title_start_y = content_y + (title_zone_h - title_block_h) // 2 if title_start_y < content_y: title_start_y = content_y # If title fits within icon height, center it vertically against the icon. # Otherwise top-align so extra lines overflow below. icon_y = title_start_y if icon_h_px > 0 and title_visual_h <= icon_h_px: title_text_y = title_start_y + (icon_h_px - title_visual_h) // 2 else: title_text_y = title_start_y # Horizontal centering: measure widest title line, center icon+gap+text block max_title_line_w = 0 for line in title_lines: bbox = draw.textbbox((0, 0), line, font=title_font) w = bbox[2] - bbox[0] if w > max_title_line_w: max_title_line_w = w title_block_w = icon_w_px + icon_gap + max_title_line_w title_block_x = content_x + (content_w - title_block_w) // 2 # Draw icon if icon_scale > 0: icon_img = generate_book_icon(48) scaled_icon = icon_img.resize((icon_w_px, icon_h_px), Image.NEAREST) for iy in range(scaled_icon.height): for ix in range(scaled_icon.width): if not scaled_icon.getpixel((ix, iy)): img.putpixel((title_block_x + ix, icon_y + iy), 0) # Draw title (to the right of the icon) title_text_x = title_block_x + icon_w_px + icon_gap current_y = title_text_y for line in title_lines: draw.text((title_text_x, current_y), line, fill=0, font=title_font) current_y += title_line_h # Wrap author author_lines = [] words = author.split() current_line = "" for word in words: test = f"{current_line} {word}".strip() bbox = draw.textbbox((0, 0), test, font=author_font) if bbox[2] - bbox[0] <= content_w: current_line = test else: if current_line: author_lines.append(current_line) current_line = word if current_line: author_lines.append(current_line) author_lines = author_lines[:3] # Draw author centered in bottom 1/3 author_line_h = 24 * author_scale # Ubuntu 10 regular advanceY ~24 author_block_h = len(author_lines) * author_line_h author_start_y = author_zone_y + (author_zone_h - author_block_h) // 2 for line in author_lines: bbox = draw.textbbox((0, 0), line, font=author_font) line_w = bbox[2] - bbox[0] line_x = content_x + (content_w - line_w) // 2 draw.text((line_x, author_start_y), line, fill=0, font=author_font) author_start_y += author_line_h return img if __name__ == "__main__": # Full cover img = create_preview(480, 800, "A Really Long Book Title That Should Wrap", "Jane Doe") img.save("mod/preview_cover_480x800.png") print("Saved mod/preview_cover_480x800.png", file=sys.stderr) # Medium thumbnail img2 = create_preview(240, 400, "A Really Long Book Title That Should Wrap", "Jane Doe") img2.save("mod/preview_thumb_240x400.png") print("Saved mod/preview_thumb_240x400.png", file=sys.stderr) # Small thumbnail img3 = create_preview(136, 226, "A Really Long Book Title", "Jane Doe") img3.save("mod/preview_thumb_136x226.png") print("Saved mod/preview_thumb_136x226.png", file=sys.stderr)