File size: 12,876 Bytes
4bb9069
d228978
 
 
 
1b1b228
d228978
 
1b1b228
d228978
 
 
 
1b1b228
d228978
 
1b1b228
 
 
 
 
d228978
1b1b228
d228978
1b1b228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bb9069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b1b228
4bb9069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d228978
 
4bb9069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b1b228
 
4bb9069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d228978
1b1b228
 
d228978
4bb9069
d228978
1b1b228
 
 
d228978
 
 
 
1b1b228
 
 
 
d228978
1b1b228
 
 
 
d228978
4bb9069
1b1b228
4bb9069
1b1b228
 
 
 
 
4bb9069
1b1b228
4bb9069
1b1b228
d228978
4bb9069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d228978
 
4bb9069
d228978
4bb9069
d228978
1b1b228
d228978
4bb9069
 
 
d228978
 
1b1b228
4bb9069
1b1b228
d228978
 
4bb9069
 
 
 
 
1b1b228
d228978
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327

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"<b>Trim size:</b> {trim_size}<br>"
                f"<b>Pages:</b> {page_count}<br>"
                f"<b>Page dimensions (in):</b> {width} x {height}<br>"
                f"<b>Margins:</b> Inside={inside}, Out={outside}, Top/Bottom={top}<br>"
                f"<b>Bleed:</b> {'Yes' if has_bleed else 'No'}<br>",
                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"<b>Trim size:</b> {trim['size']} ({size_type})<br>"
            f"<b>Pages:</b> {page_count}<br>"
            f"<b>Spine width:</b> {spine:.4f}\"<br>"
            f"<b>Page dimensions:</b> {width}\" x {height}\"<br>"
            f"<b>Page dimensions (w/bleed):</b> {width_b}\" x {height_b}\"<br>"
            "<b>Margins (no bleed):</b> " +
            f"Inside {inside}\", Out {outside}\", Top/Bottom {top}\"<br>"
            "<b>Margins (with bleed):</b> " +
            f"Inside {inside_b}\", Out {outside_b}\", Top/Bottom {top_b}\"<br>"
            f"<b>Paper:</b> {paper_type}, <b>Ink:</b> {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()