Every ebook needs two covers: one for KDP (1600x2560px at 300 DPI) and one for Gumroad (1280x720px). Designing both by hand for every book iteration wastes time. This article walks through a Python pipeline that calls an image generation API, post-processes the result with Pillow, validates dimensions and file size, and saves both variants automatically.
Why Dimensions Matter
KDP rejects covers that don't meet their specs. The required ratio is 1:1.6 (width:height). At 1600x2560px and 300 DPI, the physical print size is roughly 5.3" x 8.5" — standard trade paperback. Gumroad thumbnails display at 1280x720px (16:9), which is entirely different. If you upload your KDP cover to Gumroad, it will be cropped or letterboxed.
File size limits also apply: KDP caps cover images at 50MB (you'll rarely hit this with JPEG), and Gumroad has a 50MB limit for product thumbnails. Always validate before uploading.
Calling an Image Generation API
The pattern below works with any REST-based image generation API (Imagen, Stability AI, DALL-E, etc.). Swap the endpoint and auth header for your provider.
import os
import requests
import base64
from pathlib import Path
API_URL = "https://your-image-api-endpoint/generate"
API_KEY = os.environ["IMAGE_API_KEY"]
def generate_cover_image(prompt: str) -> bytes:
"""Call image generation API and return raw image bytes."""
payload = {
"prompt": prompt,
"width": 1600,
"height": 2560,
"num_outputs": 1,
"output_format": "png",
}
headers = {"Authorization": f"Bearer {API_KEY}"}
response = requests.post(API_URL, json=payload, headers=headers, timeout=120)
response.raise_for_status()
data = response.json()
# Most APIs return base64-encoded image data
image_b64 = data["images"][0]
return base64.b64decode(image_b64)
Save the returned bytes to disk before any post-processing. Never process in-memory only — if the next step crashes, you lose the API call.
def save_raw(image_bytes: bytes, path: Path) -> None:
path.write_bytes(image_bytes)
print(f"Saved raw image: {path} ({len(image_bytes) / 1024:.1f} KB)")
Pillow Post-Processing
Raw API output is rarely production-ready. You need to resize, set DPI metadata, add a text overlay, and export at the correct quality.
from PIL import Image, ImageDraw, ImageFont
import io
def add_text_overlay(img: Image.Image, title: str, subtitle: str) -> Image.Image:
"""Add title and subtitle text to the bottom third of the cover."""
draw = ImageDraw.Draw(img)
width, height = img.size
# Semi-transparent dark band at the bottom
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay)
overlay_draw.rectangle(
[(0, int(height * 0.65)), (width, height)],
fill=(0, 0, 0, 180),
)
img = Image.alpha_composite(img.convert("RGBA"), overlay)
draw = ImageDraw.Draw(img)
# Use a default PIL font — replace with a TTF for production
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 80)
sub_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 48)
except OSError:
title_font = ImageFont.load_default()
sub_font = ImageFont.load_default()
# Center title
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_x = (width - (title_bbox[2] - title_bbox[0])) // 2
draw.text((title_x, int(height * 0.70)), title, fill="white", font=title_font)
# Center subtitle
sub_bbox = draw.textbbox((0, 0), subtitle, font=sub_font)
sub_x = (width - (sub_bbox[2] - sub_bbox[0])) // 2
draw.text((sub_x, int(height * 0.82)), subtitle, fill="#cccccc", font=sub_font)
return img.convert("RGB")
Saving Both Variants
From one generated image, produce both the KDP and Gumroad versions:
def save_kdp_cover(img: Image.Image, output_path: Path) -> Path:
"""Resize to 1600x2560 at 300 DPI and save as JPEG."""
kdp = img.resize((1600, 2560), Image.LANCZOS)
kdp.save(output_path, format="JPEG", quality=95, dpi=(300, 300))
print(f"KDP cover saved: {output_path}")
return output_path
def save_gumroad_cover(img: Image.Image, output_path: Path) -> Path:
"""
Crop center of the KDP cover to 16:9, then resize to 1280x720.
The KDP cover is portrait; we take the upper portion for Gumroad.
"""
width, height = img.size
target_h = int(width * 720 / 1280) # height that gives 16:9 at this width
top = int(height * 0.10) # start 10% down to avoid empty sky
crop_box = (0, top, width, top + target_h)
cropped = img.crop(crop_box)
gumroad = cropped.resize((1280, 720), Image.LANCZOS)
gumroad.save(output_path, format="JPEG", quality=90)
print(f"Gumroad cover saved: {output_path}")
return output_path
File Validation Before Use
Before shipping covers to KDP or uploading to Gumroad, validate dimensions and file size programmatically:
def validate_cover(path: Path, expected_w: int, expected_h: int, max_mb: float = 50.0) -> bool:
"""Return True if the cover meets dimension and size requirements."""
errors = []
size_mb = path.stat().st_size / (1024 * 1024)
if size_mb > max_mb:
errors.append(f"File too large: {size_mb:.1f} MB (max {max_mb} MB)")
with Image.open(path) as img:
w, h = img.size
if w != expected_w or h != expected_h:
errors.append(f"Wrong dimensions: {w}x{h} (expected {expected_w}x{expected_h})")
if errors:
for e in errors:
print(f"VALIDATION ERROR: {e}")
return False
print(f"Validation passed: {path.name} ({size_mb:.2f} MB, {expected_w}x{expected_h})")
return True
# Usage
kdp_path = Path("outputs/cover_kdp.jpg")
gumrd_path = Path("outputs/cover_gumroad.jpg")
assert validate_cover(kdp_path, 1600, 2560, max_mb=50)
assert validate_cover(gumrd_path, 1280, 720, max_mb=50)
Putting It Together
def run_cover_pipeline(title: str, subtitle: str, prompt: str) -> None:
out = Path("outputs")
out.mkdir(exist_ok=True)
raw_bytes = generate_cover_image(prompt)
raw_path = out / "cover_raw.png"
save_raw(raw_bytes, raw_path)
with Image.open(raw_path) as base_img:
final = add_text_overlay(base_img.copy(), title, subtitle)
kdp_path = out / "cover_kdp.jpg"
gumrd_path = out / "cover_gumroad.jpg"
save_kdp_cover(final, kdp_path)
save_gumroad_cover(final, gumrd_path)
validate_cover(kdp_path, 1600, 2560)
validate_cover(gumrd_path, 1280, 720)
if __name__ == "__main__":
run_cover_pipeline(
title="Python Automation",
subtitle="Build pipelines that ship",
prompt="Minimalist tech book cover, dark background, glowing circuit patterns, professional",
)
The entire pipeline runs in under 90 seconds including the API call. Swap the font paths for your system and drop in your preferred image API credentials. The validation step will catch any spec violations before you waste time on a KDP rejection.
Full pipeline + source code: germy5.gumroad.com/l/xhxkzz — $19.99, 30-day refund.
Top comments (0)