import gradio as gr
import os
import tempfile
# For Word processing
try:
from docx import Document
from docx.shared import Inches
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
# For PDF processing
try:
import PyPDF2
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.pagesizes import inch
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
# ---------------- KDP Spec Config ---------------------
TRIM_SIZES = [
{"size": "5\" x 8\"", "width": 5.0, "height": 8.0, "min": 24, "max": 828},
{"size": "5.06\" x 7.81\"", "width": 5.06, "height": 7.81, "min": 24, "max": 828},
{"size": "5.25\" x 8\"", "width": 5.25, "height": 8.0, "min": 24, "max": 828},
{"size": "5.5\" x 8.5\"", "width": 5.5, "height": 8.5, "min": 24, "max": 828},
{"size": "6\" x 9\"", "width": 6.0, "height": 9.0, "min": 24, "max": 828},
# Large (cost more)
{"size": "6.14\" x 9.21\"", "width": 6.14, "height": 9.21, "min": 24, "max": 550},
{"size": "6.69\" x 9.61\"", "width": 6.69, "height": 9.61, "min": 24, "max": 550},
{"size": "7\" x 10\"", "width": 7.0, "height": 10.0, "min": 24, "max": 550},
{"size": "7.44\" x 9.69\"", "width": 7.44, "height": 9.69, "min": 24, "max": 550},
{"size": "7.5\" x 9.25\"", "width": 7.5, "height": 9.25, "min": 24, "max": 550},
{"size": "8\" x 10\"", "width": 8.0, "height": 10.0, "min": 24, "max": 550},
{"size": "8.25\" x 6\"", "width": 8.25, "height": 6.0, "min": 24, "max": 550},
{"size": "8.25\" x 8.25\"", "width": 8.25, "height": 8.25, "min": 24, "max": 550},
{"size": "8.5\" x 8.5\"", "width": 8.5, "height": 8.5, "min": 24, "max": 550},
{"size": "8.5\" x 11\"", "width": 8.5, "height": 11.0, "min": 24, "max": 550},
{"size": "8.27\" x 11.69\"", "width": 8.27, "height": 11.69, "min": 24, "max": 550},
]
def get_trim(trim_label):
for trim in TRIM_SIZES:
if trim_label.startswith(trim['size']):
return trim
return None
def get_margin(page_count, has_bleed):
"""Return inside, outside (left), top, bottom margins in inches"""
if page_count <= 150:
inside = 0.375
elif page_count <= 300:
inside = 0.5
elif page_count <= 500:
inside = 0.625
elif page_count <= 700:
inside = 0.75
else:
inside = 0.875
outside = 0.375 if has_bleed else 0.25
top = bottom = 0.375 if has_bleed else 0.25
return inside, outside, top, bottom
def get_bleed_dimensions(width, height, has_bleed):
if has_bleed:
return width + 0.25, height + 0.25
return width, height
def spine_width(page_count, paper_type, ink):
if ink == "color":
return page_count * 0.002347
elif paper_type == "cream":
return page_count * 0.0025
else:
return page_count * 0.002252
def trim_choices():
return [
f'{trim["size"]} ({"Regular" if trim["max"]==828 else "Large"})'
for trim in TRIM_SIZES
]
# -------------------- Core Formatting Logic -------------------------
def format_word(file_path, trim_size, has_bleed):
try:
if not DOCX_AVAILABLE:
return "python-docx library is required (pip install python-docx)", None
trim = get_trim(trim_size)
if not trim:
return f"Invalid trim size: {trim_size}", None
doc = Document(file_path)
text_len = sum(len(p.text) for p in doc.paragraphs)
page_count = max(24, int(text_len / 2000) or 1)
width, height = get_bleed_dimensions(trim['width'], trim['height'], has_bleed)
inside, outside, top, bottom = get_margin(page_count, has_bleed)
for section in doc.sections:
section.page_width = Inches(width)
section.page_height = Inches(height)
section.left_margin = Inches(inside)
section.right_margin = Inches(outside)
section.top_margin = Inches(top)
section.bottom_margin = Inches(bottom)
out_path = file_path.replace(".docx", "_kdp.docx")
doc.save(out_path)
return (
f"SUCCESS!\nTrim size: {trim_size}\nPage count (approx): {page_count}\nMargins (in): Inside={inside}, Out={outside}, Top/Bottom={top}\nBleed: {'Yes' if has_bleed else 'No'}",
out_path
)
except Exception as e:
return f"Error processing Word document: {str(e)}", None
def format_pdf(file_path, trim_size, has_bleed):
try:
if not REPORTLAB_AVAILABLE:
return "reportlab and PyPDF2 are required", None
trim = get_trim(trim_size)
if not trim:
return f"Invalid trim size: {trim_size}", None
with open(file_path, "rb") as f:
pdf = PyPDF2.PdfReader(f)
page_count = len(pdf.pages)
width, height = get_bleed_dimensions(trim['width'], trim['height'], has_bleed)
inside, outside, top, bottom = get_margin(page_count, has_bleed)
out_path = file_path.replace('.pdf', '_kdp.pdf')
# Create formatted PDF with specifications
doc = SimpleDocTemplate(
out_path,
pagesize=(width * inch, height * inch),
leftMargin=inside * inch,
rightMargin=outside * inch,
topMargin=top * inch,
bottomMargin=bottom * inch
)
styles = getSampleStyleSheet()
story = [
Paragraph("Amazon KDP Formatting Summary", ParagraphStyle('Heading1', fontSize=20, alignment=TA_CENTER)),
Spacer(1, 12),
Paragraph(
f"Trim size: {trim_size}
"
f"Pages: {page_count}
"
f"Page dimensions (in): {width} x {height}
"
f"Margins: Inside={inside}, Out={outside}, Top/Bottom={top}
"
f"Bleed: {'Yes' if has_bleed else 'No'}
",
styles['BodyText']
)
]
doc.build(story)
return f"SUCCESS! PDF processed and formatted for KDP!\nPages: {page_count}\nMargins applied for KDP compliance.", out_path
except Exception as e:
return f"Error processing PDF: {str(e)}", None
def format_document(file, trim_size, has_bleed, paper_type, ink_type):
try:
if file is None:
return "Please upload a .docx or .pdf file.", None
# Handle both file objects and file paths (Gradio can pass either)
if hasattr(file, 'name') and hasattr(file, 'read'):
# File object from Gradio
file_path = file.name
ext = os.path.splitext(file_path)[1].lower()
# Create temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
tmp.write(file.read())
tmp_file_path = tmp.name
elif isinstance(file, str):
# File path string
tmp_file_path = file
ext = os.path.splitext(file)[1].lower()
else:
return "Invalid file input", None
if ext == '.docx':
result, out_file = format_word(tmp_file_path, trim_size, has_bleed)
elif ext == '.pdf':
result, out_file = format_pdf(tmp_file_path, trim_size, has_bleed)
else:
return "Only .docx and .pdf files are supported", None
# Clean up temporary file if we created one
if hasattr(file, 'read') and os.path.exists(tmp_file_path):
os.unlink(tmp_file_path)
return result, out_file
except Exception as e:
return f"Error in format_document: {str(e)}", None
def calculate_specs(trim_size, page_count, paper_type, ink_type):
try:
trim = get_trim(trim_size)
if not trim:
return "Select a valid trim size."
if not (trim["min"] <= page_count <= trim["max"]):
return f"Invalid page count for {trim['size']}: must be {trim['min']}-{trim['max']}"
inside, outside, top, bottom = get_margin(page_count, False)
inside_b, outside_b, top_b, bottom_b = get_margin(page_count, True)
width, height = trim['width'], trim['height']
width_b, height_b = get_bleed_dimensions(width, height, True)
spine = spine_width(page_count, paper_type, ink_type)
size_type = "Regular" if trim["max"] == 828 else "Large"
return (
f"Trim size: {trim['size']} ({size_type})
"
f"Pages: {page_count}
"
f"Spine width: {spine:.4f}\"
"
f"Page dimensions: {width}\" x {height}\"
"
f"Page dimensions (w/bleed): {width_b}\" x {height_b}\"
"
"Margins (no bleed): " +
f"Inside {inside}\", Out {outside}\", Top/Bottom {top}\"
"
"Margins (with bleed): " +
f"Inside {inside_b}\", Out {outside_b}\", Top/Bottom {top_b}\"
"
f"Paper: {paper_type}, Ink: {ink_type}"
)
except Exception as e:
return f"Error calculating specs: {str(e)}"
# -------------------- Gradio Interface -----------------------
with gr.Blocks(title="Amazon KDP Paperback Formatter") as demo:
gr.Markdown("""
# đ Amazon KDP Paperbook Formatter
Upload a .docx or .pdf manuscript and choose your KDP trim size.
Set bleed (for graphics to the page edge), paper type and ink.
Download your ready-to-upload file.
""")
with gr.Tab("đ Format Document"):
with gr.Row():
with gr.Column():
file_input = gr.File(label="Upload .docx or .pdf", file_types=['.docx', '.pdf'])
trim_size_dropdown = gr.Dropdown(
choices=trim_choices(), label="Trim Size",
value="6\" x 9\" (Regular)"
)
has_bleed_checkbox = gr.Checkbox(label="Include Bleed", value=False)
paper_type_radio = gr.Radio(choices=["white", "cream"], label="Paper", value="white")
ink_type_radio = gr.Radio(choices=["black", "color"], label="Ink", value="black")
format_button = gr.Button("Format for KDP", variant="primary")
with gr.Column():
result_output = gr.Textbox(label="Result", lines=8)
download_output = gr.File(label="Download", visible=False)
with gr.Tab("đ Calculate Specs"):
calc_trim = gr.Dropdown(trim_choices(), label="Trim Size", value="6\" x 9\" (Regular)")
calc_page = gr.Slider(24, 828, value=100, step=1, label="Page Count")
calc_paper = gr.Radio(["white", "cream"], label="Paper", value="white")
calc_ink = gr.Radio(["black", "color"], label="Ink", value="black")
calc_output = gr.HTML(label="Specifications")
calc_btn = gr.Button("Calculate")
with gr.Tab("âšī¸ KDP Help"):
gr.Markdown("""
## Amazon KDP Paperback Guidelines
**Popular Trim Sizes:**
- **6" x 9"** â Most popular for novels/non-fiction
- **5.5" x 8.5"** Good for smaller books
- **8.5" x 11"** Workbooks, journals, reference
**Margins by Page Count:**
- 24-150 pages: 0.375" inside margin
- 151-300 pages: 0.5" inside margin
- 301-500 pages: 0.625" inside margin
- 501-700 pages: 0.75" inside margin
- 701-828 pages: 0.875" inside margin
**Outside/Top/Bottom Margins:**
- Without bleed: 0.25"
- With bleed: 0.375"
**Bleed:**
- Add 0.125" to all edges when graphics extend to page edge
- Example: 6" x 9" becomes 6.125" x 9.25" with bleed
**Spine Width:**
- B&W (White): pages à 0.002252"
- B&W (Cream): pages à 0.0025"
- Color: pages à 0.002347"
**Page Limits:**
- Regular sizes: 24-828 pages
- Large sizes: 24-550 pages
""")
# Event handlers
format_button.click(
fn=format_document,
inputs=[file_input, trim_size_dropdown, has_bleed_checkbox, paper_type_radio, ink_type_radio],
outputs=[result_output, download_output]
).then(
fn=lambda x: gr.update(visible=x is not None),
inputs=[download_output],
outputs=[download_output]
)
calc_btn.click(
fn=calculate_specs,
inputs=[calc_trim, calc_page, calc_paper, calc_ink],
outputs=[calc_output]
)
# Auto-calculate on changes
for component in [calc_trim, calc_page, calc_paper, calc_ink]:
component.change(
fn=calculate_specs,
inputs=[calc_trim, calc_page, calc_paper, calc_ink],
outputs=[calc_output]
)
if __name__ == "__main__":
demo.launch()