Spaces:
Sleeping
Sleeping
Download app.py from DeepParmar/Huggingface: direct link, hf CLI and curl.
- Browser
- Download file 41.1 kB
-
https://huggingface.co/spaces/DeepParmar/Huggingface/resolve/main/app.py
- Command line
-
hf download hf://spaces/DeepParmar/Huggingface/app.py
-
curl -L -o app.py https://huggingface.co/spaces/DeepParmar/Huggingface/resolve/main/app.py
41.1 kB
| import os | |
| import io | |
| import base64 | |
| import re | |
| import fitz # PyMuPDF | |
| from flask import Flask, render_template_string, request, send_file, jsonify | |
| app = Flask(__name__) | |
| # Font path - prefers Anek Gujarati if available, falls back to system font | |
| FONT_PATH = os.path.join(os.path.dirname(__file__), "GujaratiFont.ttf") | |
| UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "uploads") | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| HTML = """<!DOCTYPE html> | |
| <html lang="mul"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PDF નામ ઉમેરો | Invitation Editor</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Anek+Gujarati:wght@100..800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #8B2252; | |
| --primary-light: #c4547c; | |
| --accent: #d4a843; | |
| --bg: #fdf6ee; | |
| --card: #ffffff; | |
| --border: #e8d5c4; | |
| --text: #3a2a1a; | |
| --text-muted: #7a6a5a; | |
| --shadow: 0 4px 24px rgba(139,34,82,0.10); | |
| --radius: 16px; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| } | |
| /* Header */ | |
| .header { | |
| background: linear-gradient(135deg, var(--primary) 0%, #5a1236 100%); | |
| color: white; | |
| padding: 20px 24px; | |
| text-align: center; | |
| box-shadow: 0 2px 12px rgba(0,0,0,0.2); | |
| } | |
| .header h1 { | |
| font-family: 'Anek Gujarati', sans-serif; | |
| font-size: clamp(1.4rem, 4vw, 2rem); | |
| font-weight: 700; | |
| letter-spacing: 0.5px; | |
| } | |
| .header p { | |
| font-size: 0.85rem; | |
| opacity: 0.85; | |
| margin-top: 4px; | |
| } | |
| /* Main layout */ | |
| .container { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 24px 16px; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 24px; | |
| align-items: start; | |
| } | |
| @media (max-width: 700px) { | |
| .container { grid-template-columns: 1fr; } | |
| } | |
| /* Cards */ | |
| .card { | |
| background: var(--card); | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| padding: 24px; | |
| } | |
| .card-title { | |
| font-family: 'Anek Gujarati', sans-serif; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: var(--primary); | |
| margin-bottom: 18px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .card-title span { font-size: 1.3rem; } | |
| /* Form elements */ | |
| .form-group { margin-bottom: 18px; } | |
| label { | |
| display: block; | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.6px; | |
| margin-bottom: 6px; | |
| } | |
| label.gujarati-label { | |
| font-family: 'Anek Gujarati', sans-serif; | |
| font-size: 0.9rem; | |
| text-transform: none; | |
| letter-spacing: 0; | |
| } | |
| input[type="text"], input[type="file"], select { | |
| width: 100%; | |
| padding: 10px 14px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| font-family: 'Anek Gujarati', 'Inter', sans-serif; | |
| background: #fafafa; | |
| color: var(--text); | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| outline: none; | |
| } | |
| input[type="text"]:focus, select:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(139,34,82,0.08); | |
| background: #fff; | |
| } | |
| input[type="file"] { | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| } | |
| /* Font size range */ | |
| .range-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| input[type="range"] { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: linear-gradient(to right, var(--primary) 0%, var(--primary) var(--pct,50%), #e0d0d8 var(--pct,50%), #e0d0d8 100%); | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; height: 20px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| box-shadow: 0 2px 6px rgba(139,34,82,0.3); | |
| } | |
| .range-value { | |
| min-width: 36px; | |
| text-align: center; | |
| font-weight: 700; | |
| color: var(--primary); | |
| font-size: 1rem; | |
| } | |
| /* Position controls */ | |
| .position-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| } | |
| .pos-group label { margin-bottom: 4px; } | |
| .pos-group input[type="number"] { | |
| width: 100%; | |
| padding: 8px 12px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 10px; | |
| font-size: 0.95rem; | |
| background: #fafafa; | |
| color: var(--text); | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| .pos-group input[type="number"]:focus { | |
| border-color: var(--primary); | |
| background: #fff; | |
| } | |
| /* Color picker row */ | |
| .color-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="color"] { | |
| width: 44px; | |
| height: 38px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| padding: 2px; | |
| background: #fafafa; | |
| } | |
| .color-label { font-size: 0.85rem; color: var(--text-muted); } | |
| /* Buttons */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 12px 22px; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| cursor: pointer; | |
| border: none; | |
| transition: all 0.2s; | |
| font-family: 'Anek Gujarati', 'Inter', sans-serif; | |
| width: 100%; | |
| margin-bottom: 10px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), #6a1040); | |
| color: white; | |
| box-shadow: 0 3px 12px rgba(139,34,82,0.25); | |
| } | |
| .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 5px 16px rgba(139,34,82,0.35); } | |
| .btn-primary:active { transform: translateY(0); } | |
| .btn-accent { | |
| background: linear-gradient(135deg, var(--accent), #b8862a); | |
| color: white; | |
| box-shadow: 0 3px 12px rgba(212,168,67,0.3); | |
| } | |
| .btn-accent:hover { transform: translateY(-1px); } | |
| .btn-outline { | |
| background: white; | |
| color: var(--primary); | |
| border: 2px solid var(--primary); | |
| } | |
| .btn-outline:hover { background: var(--bg); } | |
| .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; } | |
| /* Preview area */ | |
| .preview-area { | |
| background: #f0e8e0; | |
| border-radius: 12px; | |
| min-height: 300px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| position: relative; | |
| margin-bottom: 16px; | |
| border: 1.5px solid var(--border); | |
| } | |
| .preview-area img { | |
| max-width: 100%; | |
| max-height: 500px; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| display: block; | |
| } | |
| .preview-placeholder { | |
| text-align: center; | |
| color: var(--text-muted); | |
| padding: 40px 20px; | |
| } | |
| .preview-placeholder .icon { font-size: 3rem; margin-bottom: 12px; } | |
| .preview-placeholder p { font-size: 0.9rem; } | |
| /* Page selector */ | |
| .page-tabs { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 14px; | |
| } | |
| .page-tab { | |
| flex: 1; | |
| padding: 8px; | |
| border: 1.5px solid var(--border); | |
| border-radius: 8px; | |
| background: white; | |
| cursor: pointer; | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| text-align: center; | |
| transition: all 0.2s; | |
| } | |
| .page-tab.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| /* Status / alerts */ | |
| .alert { | |
| padding: 10px 14px; | |
| border-radius: 8px; | |
| font-size: 0.85rem; | |
| margin-bottom: 14px; | |
| display: none; | |
| } | |
| .alert.success { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; } | |
| .alert.error { background: #fce4ec; color: #c62828; border: 1px solid #ef9a9a; } | |
| .alert.info { background: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; } | |
| .alert.show { display: block; } | |
| /* Loading */ | |
| .spinner { | |
| display: inline-block; | |
| width: 18px; height: 18px; | |
| border: 2.5px solid rgba(255,255,255,0.3); | |
| border-top-color: white; | |
| border-radius: 50%; | |
| animation: spin 0.7s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .loading { display: none; } | |
| /* Preview loading overlay */ | |
| .preview-loading { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(255,255,255,0.85); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 12px; | |
| display: none; | |
| } | |
| .preview-loading.show { display: flex; } | |
| .spinner-dark { | |
| width: 32px; height: 32px; | |
| border: 3px solid rgba(139,34,82,0.2); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| /* Divider */ | |
| .divider { | |
| border: none; | |
| border-top: 1.5px solid var(--border); | |
| margin: 18px 0; | |
| } | |
| /* Info badge */ | |
| .info-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| background: #fff3e0; | |
| color: #e65100; | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| margin-bottom: 14px; | |
| } | |
| /* Filename preview */ | |
| .filename-preview { | |
| background: #f5f0fb; | |
| border: 1.5px dashed var(--primary-light); | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| font-size: 0.82rem; | |
| color: var(--primary); | |
| font-weight: 600; | |
| margin-top: 6px; | |
| font-family: monospace; | |
| } | |
| /* Boldness scale */ | |
| .boldness-tag { | |
| background: var(--primary); | |
| color: white; | |
| padding: 2px 10px; | |
| border-radius: 20px; | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| vertical-align: middle; | |
| } | |
| .boldness-scale { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 4px; | |
| font-size: 0.68rem; | |
| color: var(--text-muted); | |
| } | |
| /* Convert button */ | |
| .btn-convert { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| margin-top: 8px; | |
| padding: 8px 16px; | |
| background: linear-gradient(135deg, #1a6b3c, #0f4d2a); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.88rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| font-family: 'Anek Gujarati', 'Inter', sans-serif; | |
| transition: all 0.2s; | |
| box-shadow: 0 2px 8px rgba(26,107,60,0.25); | |
| } | |
| .btn-convert:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(26,107,60,0.35); } | |
| .btn-convert:active { transform: translateY(0); } | |
| .btn-convert:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } | |
| /* Lock PDF */ | |
| .lock-bar { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 12px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .btn-lock { | |
| display: inline-flex; align-items: center; gap: 6px; | |
| padding: 8px 14px; | |
| border: none; border-radius: 8px; | |
| font-size: 0.82rem; font-weight: 600; cursor: pointer; | |
| font-family: 'Anek Gujarati','Inter',sans-serif; | |
| transition: all 0.2s; | |
| } | |
| .btn-lock-on { background: #1a3a6b; color: white; box-shadow: 0 2px 8px rgba(26,58,107,0.25); } | |
| .btn-lock-on:hover { background: #142d54; } | |
| .btn-lock-off { background: #b71c1c; color: white; box-shadow: 0 2px 8px rgba(183,28,28,0.25); } | |
| .btn-lock-off:hover { background: #8b0000; } | |
| .lock-status { | |
| font-size: 0.78rem; color: var(--text-muted); | |
| flex: 1; min-width: 120px; | |
| } | |
| .lock-status.locked { color: #1a6b3c; font-weight: 600; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>✉️ ચાંદલા વિધી — આમંત્રણ સંપાદક</h1> | |
| <p>PDF invitation editor • Add recipient name to invitation</p> | |
| </div> | |
| <div class="container"> | |
| <!-- LEFT: Settings Panel --> | |
| <div> | |
| <!-- Step 1: Upload PDF --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title"><span>📄</span> Step 1: PDF પસંદ કરો</div> | |
| <div id="lockedBanner" style="display:none;" class="alert success" style="margin-bottom:10px;"> | |
| 🔒 PDF લૉક છે — આ PDF આપોઆપ લોડ થઈ છે | |
| </div> | |
| <div class="form-group"> | |
| <label>આમંત્રણ PDF ફાઇલ અપલોડ કરો</label> | |
| <input type="file" id="pdfFile" accept=".pdf" onchange="handlePDFUpload(this)"> | |
| </div> | |
| <div id="uploadStatus" class="alert info">📋 PDF લોડ થઈ ગઈ — <span id="pageCount"></span> pages</div> | |
| <div class="lock-bar"> | |
| <button class="btn-lock btn-lock-on" id="lockBtn" onclick="lockPDF()" title="Save this PDF permanently on server"> | |
| 🔒 Lock PDF | |
| </button> | |
| <button class="btn-lock btn-lock-off" id="unlockBtn" onclick="unlockPDF()" style="display:none;" title="Remove saved PDF"> | |
| 🔓 Unlock PDF | |
| </button> | |
| <span class="lock-status" id="lockStatus">PDF lock કરો — દર વખતે upload નહીં કરવું પડે</span> | |
| </div> | |
| </div> | |
| <!-- Step 2: Name Input --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title"><span>✍️</span> Step 2: નામ ટાઇપ કરો</div> | |
| <div class="form-group"> | |
| <label class="gujarati-label">સ્નેહીશ્રી, ______________ (નામ)</label> | |
| <input type="text" id="nameText" lang="en" | |
| placeholder="Type English name & click Convert → or type Gujarati directly" | |
| autocomplete="off" spellcheck="false" | |
| oninput="updateFilenamePreview(); autoPreview()"> | |
| <button class="btn-convert" onclick="convertToGujarati()" id="convertBtn" type="button"> | |
| <span id="convertSpinner" style="display:none;">⏳</span> | |
| <span id="convertLabel">🔤→ ગુ Convert to Gujarati</span> | |
| </button> | |
| <div id="convertAlert" style="display:none; font-size:0.8rem; color:#c62828; margin-top:4px;"></div> | |
| <div class="filename-preview" id="filenamePreview">📁 filename.pdf</div> | |
| </div> | |
| <hr class="divider"> | |
| <!-- Font Size --> | |
| <div class="form-group"> | |
| <label>ફૉન્ટ સાઈઝ (Font Size)</label> | |
| <div class="range-row"> | |
| <input type="range" id="fontSize" min="10" max="40" value="18" | |
| oninput="updateFontSizeDisplay(); autoPreview()"> | |
| <div class="range-value" id="fontSizeValue">18</div> | |
| </div> | |
| </div> | |
| <!-- Boldness --> | |
| <div class="form-group"> | |
| <label>જાડાઈ (Boldness) <span id="boldnessLabel" class="boldness-tag">Normal</span></label> | |
| <div class="range-row"> | |
| <input type="range" id="boldness" min="1" max="5" value="1" | |
| oninput="updateBoldnessDisplay(); autoPreview()"> | |
| <div class="range-value" id="boldnessValue">1</div> | |
| </div> | |
| <div class="boldness-scale"> | |
| <span>Normal</span><span>Light</span><span>Medium</span><span>Bold</span><span>Extra Bold</span> | |
| </div> | |
| </div> | |
| <!-- Text Color --> | |
| <div class="form-group"> | |
| <label>ટેક્સ્ટ રંગ (Text Color)</label> | |
| <div class="color-row"> | |
| <input type="color" id="textColor" value="#7a1a3a" | |
| onchange="autoPreview()"> | |
| <span class="color-label">ઇન્વિટેશન ટેક્સ્ટ માટે ઘાટો રંગ સૂચવવામાં આવે છે</span> | |
| </div> | |
| </div> | |
| <hr class="divider"> | |
| <!-- Position Controls --> | |
| <div class="info-badge">📍 ટેક્સ્ટ સ્થિતિ (Position)</div> | |
| <div class="position-grid"> | |
| <div class="pos-group"> | |
| <label>X (→ ડાબે-જમણે)</label> | |
| <input type="number" id="posX" value="165" min="0" max="600" | |
| oninput="autoPreview()"> | |
| </div> | |
| <div class="pos-group"> | |
| <label>Y (↓ ઉપર-નીચે)</label> | |
| <input type="number" id="posY" value="340" min="0" max="1000" | |
| oninput="autoPreview()"> | |
| </div> | |
| </div> | |
| <p style="font-size:0.75rem;color:var(--text-muted);margin-top:6px;"> | |
| 💡 પ્રિવ્યૂ જોઈ X/Y બદલો જ્યાં "સ્નેહીશ્રી," ની પછી ખાલી જગ્યા છે | |
| </p> | |
| </div> | |
| <!-- Step 3: Actions --> | |
| <div class="card"> | |
| <div class="card-title"><span>🎯</span> Step 3: Preview & Download</div> | |
| <div id="actionAlert" class="alert error"></div> | |
| <button class="btn btn-primary" onclick="generatePreview()" id="previewBtn"> | |
| <span class="loading" id="previewLoad"><span class="spinner"></span></span> | |
| 👁️ Preview જુઓ (Page 2) | |
| </button> | |
| <button class="btn btn-accent" onclick="downloadPDF()" id="downloadBtn"> | |
| <span class="loading" id="downloadLoad"><span class="spinner"></span></span> | |
| ⬇️ PDF Download કરો | |
| </button> | |
| <button class="btn btn-outline" onclick="previewPage(1)" id="page1Btn"> | |
| 📋 Page 1 (Cover) જુઓ | |
| </button> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Preview Panel --> | |
| <div class="card"> | |
| <div class="card-title"><span>🖼️</span> PDF Preview</div> | |
| <div class="page-tabs"> | |
| <div class="page-tab active" id="tab1" onclick="switchTab(1)">📄 Page 1 (Cover)</div> | |
| <div class="page-tab" id="tab2" onclick="switchTab(2)">📝 Page 2 (Details)</div> | |
| </div> | |
| <div class="preview-area" id="previewArea"> | |
| <div class="preview-loading" id="previewLoading"> | |
| <div class="spinner-dark"></div> | |
| </div> | |
| <div class="preview-placeholder" id="previewPlaceholder"> | |
| <div class="icon">📋</div> | |
| <p>PDF અપલોડ કરો અને<br>Preview બટન દબાવો</p> | |
| <p style="margin-top:8px;font-size:0.78rem;opacity:0.6"> | |
| Page 2 પર "સ્નેહીશ્રી," ની<br>પછી નામ ઉમેરાશે | |
| </p> | |
| </div> | |
| <img id="previewImg" style="display:none;" alt="PDF Preview"> | |
| </div> | |
| <div class="alert info show" id="previewNote" style="font-size:0.8rem;"> | |
| ℹ️ <strong>Page 2</strong> પર "સ્નેહીશ્રી,........" ની ખાલી જગ્યામાં નામ ઉમેરવામાં આવશે.<br> | |
| X=165, Y=340 ડિફૉલ્ટ position છે — preview જોઈ adjust કરો. | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let pdfData = null; | |
| let currentTab = 2; | |
| let autoPreviewTimer = null; | |
| // ── Font size display ────────────────────────────────────── | |
| function updateFontSizeDisplay() { | |
| const val = document.getElementById('fontSize').value; | |
| document.getElementById('fontSizeValue').textContent = val; | |
| // Update range gradient | |
| const min = 10, max = 40; | |
| const pct = ((val - min) / (max - min)) * 100; | |
| document.getElementById('fontSize').style.setProperty('--pct', pct + '%'); | |
| } | |
| // ── Filename preview ─────────────────────────────────────── | |
| function updateFilenamePreview() { | |
| const name = document.getElementById('nameText').value.trim(); | |
| const safe = name.replace(/[^\\w\\s\\u0A80-\\u0AFF]/g, '').replace(/\\s+/g, '_') || 'invitation'; | |
| document.getElementById('filenamePreview').textContent = '📁 ' + safe + '.pdf'; | |
| } | |
| // ── Convert English → Gujarati ───────────────────────────── | |
| async function convertToGujarati() { | |
| const input = document.getElementById('nameText'); | |
| const text = input.value.trim(); | |
| if (!text) { | |
| showConvertAlert('⚠️ પ્રથમ English નામ ટાઇપ કરો!'); | |
| return; | |
| } | |
| // If already Gujarati, skip | |
| if (/[\\u0A80-\\u0AFF]/.test(text)) { | |
| showConvertAlert('ℹ️ ટેક્સ્ટ પહેલેથી ગુજરાતીમાં છે!'); | |
| return; | |
| } | |
| const btn = document.getElementById('convertBtn'); | |
| const spinner = document.getElementById('convertSpinner'); | |
| const label = document.getElementById('convertLabel'); | |
| btn.disabled = true; | |
| spinner.style.display = 'inline'; | |
| label.textContent = 'Converting...'; | |
| hideConvertAlert(); | |
| try { | |
| const resp = await fetch('/transliterate', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({text}) | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) { | |
| showConvertAlert('❌ ' + data.error); | |
| } else if (data.result) { | |
| input.value = data.result; | |
| updateFilenamePreview(); | |
| autoPreview(); | |
| } else { | |
| showConvertAlert('❌ Conversion failed. Try typing Gujarati directly.'); | |
| } | |
| } catch(e) { | |
| showConvertAlert('❌ Network error: ' + e.message); | |
| } finally { | |
| btn.disabled = false; | |
| spinner.style.display = 'none'; | |
| label.textContent = '🔤→ ગુ Convert to Gujarati'; | |
| } | |
| } | |
| function showConvertAlert(msg) { | |
| const el = document.getElementById('convertAlert'); | |
| el.textContent = msg; | |
| el.style.display = 'block'; | |
| setTimeout(() => el.style.display = 'none', 4000); | |
| } | |
| function hideConvertAlert() { | |
| document.getElementById('convertAlert').style.display = 'none'; | |
| } | |
| // ── PDF Upload ───────────────────────────────────────────── | |
| function handlePDFUpload(input) { | |
| const file = input.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| pdfData = e.target.result.split(',')[1]; // base64 | |
| document.getElementById('uploadStatus').className = 'alert info show'; | |
| document.getElementById('uploadStatus').innerHTML = '✅ PDF লোডেড: <strong>' + file.name + '</strong>'; | |
| // Auto preview page 2 | |
| setTimeout(() => generatePreview(), 300); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| // ── Auto preview with debounce ───────────────────────────── | |
| function autoPreview() { | |
| if (!pdfData) return; | |
| clearTimeout(autoPreviewTimer); | |
| autoPreviewTimer = setTimeout(() => generatePreview(), 700); | |
| } | |
| // ── Switch page tab ──────────────────────────────────────── | |
| function switchTab(n) { | |
| currentTab = n; | |
| document.getElementById('tab1').classList.toggle('active', n === 1); | |
| document.getElementById('tab2').classList.toggle('active', n === 2); | |
| if (pdfData) generatePreview(); | |
| } | |
| // ── Preview ──────────────────────────────────────────────── | |
| async function generatePreview() { | |
| if (!pdfData) { | |
| showAlert('actionAlert', 'error', '⚠️ પ્રથમ PDF ફાઇલ અપલોડ કરો!'); | |
| return; | |
| } | |
| hideAlert('actionAlert'); | |
| setLoading('previewLoad', true); | |
| document.getElementById('previewLoading').classList.add('show'); | |
| try { | |
| const params = getParams(); | |
| params.page = currentTab; | |
| const resp = await fetch('/preview', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(params) | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) { | |
| showAlert('actionAlert', 'error', '❌ ' + data.error); | |
| } else { | |
| const img = document.getElementById('previewImg'); | |
| img.src = 'data:image/png;base64,' + data.image; | |
| img.style.display = 'block'; | |
| document.getElementById('previewPlaceholder').style.display = 'none'; | |
| } | |
| } catch(e) { | |
| showAlert('actionAlert', 'error', '❌ Preview error: ' + e.message); | |
| } finally { | |
| setLoading('previewLoad', false); | |
| document.getElementById('previewLoading').classList.remove('show'); | |
| } | |
| } | |
| async function previewPage(n) { | |
| currentTab = n; | |
| document.getElementById('tab1').classList.toggle('active', n===1); | |
| document.getElementById('tab2').classList.toggle('active', n===2); | |
| await generatePreview(); | |
| } | |
| // ── Download ─────────────────────────────────────────────── | |
| async function downloadPDF() { | |
| if (!pdfData) { | |
| showAlert('actionAlert', 'error', '⚠️ પ્રથમ PDF ફાઇલ અપલોડ કરો!'); | |
| return; | |
| } | |
| const name = document.getElementById('nameText').value.trim(); | |
| if (!name) { | |
| showAlert('actionAlert', 'error', '⚠️ કૃપા કરી નામ ટાઇપ કરો!'); | |
| return; | |
| } | |
| hideAlert('actionAlert'); | |
| setLoading('downloadLoad', true); | |
| try { | |
| const params = getParams(); | |
| const resp = await fetch('/download', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(params) | |
| }); | |
| if (!resp.ok) { | |
| const data = await resp.json(); | |
| showAlert('actionAlert', 'error', '❌ ' + (data.error || 'Download failed')); | |
| return; | |
| } | |
| const blob = await resp.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| const safe = name.replace(/[^\\w\\s\\u0A80-\\u0AFF]/g, '').replace(/\\s+/g, '_') || 'invitation'; | |
| a.href = url; | |
| a.download = safe + '.pdf'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showAlert('actionAlert', 'success', '✅ PDF Download થઈ ગઈ! (' + safe + '.pdf)'); | |
| } catch(e) { | |
| showAlert('actionAlert', 'error', '❌ Download error: ' + e.message); | |
| } finally { | |
| setLoading('downloadLoad', false); | |
| } | |
| } | |
| // ── Helpers ──────────────────────────────────────────────── | |
| function getParams() { | |
| const color = document.getElementById('textColor').value; | |
| const r = parseInt(color.slice(1,3),16)/255; | |
| const g = parseInt(color.slice(3,5),16)/255; | |
| const b = parseInt(color.slice(5,7),16)/255; | |
| return { | |
| pdf: pdfData, | |
| name: document.getElementById('nameText').value, | |
| fontSize: parseInt(document.getElementById('fontSize').value), | |
| posX: parseInt(document.getElementById('posX').value), | |
| posY: parseInt(document.getElementById('posY').value), | |
| colorR: r, colorG: g, colorB: b, | |
| boldness: parseInt(document.getElementById('boldness').value) | |
| }; | |
| } | |
| // ── Boldness display ─────────────────────────────────────── | |
| const boldnessNames = ['', 'Normal', 'Light Bold', 'Medium', 'Bold', 'Extra Bold']; | |
| function updateBoldnessDisplay() { | |
| const val = parseInt(document.getElementById('boldness').value); | |
| document.getElementById('boldnessValue').textContent = val; | |
| document.getElementById('boldnessLabel').textContent = boldnessNames[val]; | |
| const min = 1, max = 5; | |
| const pct = ((val - min) / (max - min)) * 100; | |
| document.getElementById('boldness').style.setProperty('--pct', pct + '%'); | |
| } | |
| // ── Lock PDF ─────────────────────────────────────────────── | |
| async function lockPDF() { | |
| if (!pdfData) { | |
| alert('⚠️ પ્રથમ PDF upload કરો!'); | |
| return; | |
| } | |
| try { | |
| const resp = await fetch('/lock_pdf', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({pdf: pdfData}) | |
| }); | |
| const data = await resp.json(); | |
| if (data.ok) { | |
| setLockUI(true); | |
| } else { | |
| alert('❌ Lock failed: ' + (data.error || 'Unknown error')); | |
| } | |
| } catch(e) { | |
| alert('❌ ' + e.message); | |
| } | |
| } | |
| async function unlockPDF() { | |
| try { | |
| const resp = await fetch('/unlock_pdf', {method: 'POST'}); | |
| const data = await resp.json(); | |
| if (data.ok) setLockUI(false); | |
| } catch(e) { | |
| alert('❌ ' + e.message); | |
| } | |
| } | |
| function setLockUI(locked) { | |
| document.getElementById('lockBtn').style.display = locked ? 'none' : 'inline-flex'; | |
| document.getElementById('unlockBtn').style.display = locked ? 'inline-flex' : 'none'; | |
| document.getElementById('lockStatus').textContent = locked | |
| ? '✅ PDF સર્વર પર save છે — browser બંધ કરો, ફરી ખોલો, PDF હશે!' | |
| : 'PDF lock કરો — દર વખતે upload નહીં કરવું પડે'; | |
| document.getElementById('lockStatus').className = 'lock-status' + (locked ? ' locked' : ''); | |
| document.getElementById('lockedBanner').style.display = locked ? 'block' : 'none'; | |
| } | |
| // ── Auto-load locked PDF on page start ──────────────────── | |
| async function checkLockedPDF() { | |
| try { | |
| const resp = await fetch('/get_locked_pdf'); | |
| if (!resp.ok) return; | |
| const data = await resp.json(); | |
| if (data.pdf) { | |
| pdfData = data.pdf; | |
| document.getElementById('uploadStatus').className = 'alert info show'; | |
| document.getElementById('uploadStatus').innerHTML = | |
| '🔒 Locked PDF આપોઆપ લોડ થઈ: <strong>' + (data.filename || 'invitation.pdf') + '</strong>'; | |
| setLockUI(true); | |
| setTimeout(() => generatePreview(), 400); | |
| } | |
| } catch(e) { /* no locked PDF, that's fine */ } | |
| } | |
| function setLoading(id, on) { | |
| document.getElementById(id).style.display = on ? 'inline-block' : 'none'; | |
| } | |
| function showAlert(id, type, msg) { | |
| const el = document.getElementById(id); | |
| el.className = 'alert ' + type + ' show'; | |
| el.textContent = msg; | |
| } | |
| function hideAlert(id) { | |
| document.getElementById(id).className = 'alert'; | |
| } | |
| // Init | |
| updateFontSizeDisplay(); | |
| updateBoldnessDisplay(); | |
| updateFilenamePreview(); | |
| document.getElementById('fontSize').addEventListener('input', updateFontSizeDisplay); | |
| document.getElementById('boldness').addEventListener('input', updateBoldnessDisplay); | |
| checkLockedPDF(); | |
| </script> | |
| </body> | |
| </html>""" | |
| def hex_to_rgb_float(hex_color): | |
| hex_color = hex_color.lstrip('#') | |
| return tuple(int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4)) | |
| def is_gujarati(text): | |
| """Return True if text contains any Gujarati Unicode characters (U+0A80–U+0AFF).""" | |
| return any('\u0A80' <= ch <= '\u0AFF' for ch in text) | |
| def render_text_as_image(text, font_path, font_size_pt, color_float, boldness=1): | |
| """ | |
| Render text to a transparent PNG using Pillow+FreeType (handles Gujarati shaping). | |
| Returns (png_bytes, img_w_px, img_h_px). | |
| """ | |
| from PIL import Image, ImageDraw, ImageFont | |
| # Convert PDF points → screen pixels (96 DPI) | |
| font_size_px = max(8, int(font_size_pt * 96 / 72)) | |
| color_rgba = tuple(int(c * 255) for c in color_float) + (255,) | |
| try: | |
| font = ImageFont.truetype(font_path, font_size_px) | |
| except Exception: | |
| font = ImageFont.load_default() | |
| # Measure exact text bounding box | |
| probe = Image.new('RGBA', (4000, 400), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(probe) | |
| bbox = draw.textbbox((0, 0), text, font=font) # (left, top, right, bottom) | |
| text_w = max(bbox[2] - bbox[0], 1) | |
| text_h = max(bbox[3] - bbox[1], 1) | |
| pad = 6 | |
| extra = boldness - 1 # extra horizontal pixels for simulated bold | |
| img_w = text_w + pad * 2 + extra + 4 | |
| img_h = text_h + pad * 2 | |
| img = Image.new('RGBA', (img_w, img_h), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| # Draw text `boldness` times with tiny horizontal offsets to simulate weight | |
| step = 0.5 | |
| for i in range(boldness): | |
| draw.text( | |
| (pad - bbox[0] + i * step, pad - bbox[1]), | |
| text, font=font, fill=color_rgba | |
| ) | |
| buf = io.BytesIO() | |
| img.save(buf, format='PNG') | |
| buf.seek(0) | |
| return buf.getvalue(), img_w, img_h | |
| def process_pdf(pdf_bytes, name_text, font_size=18, pos_x=165, pos_y=340, | |
| color=(0.48, 0.10, 0.23), page_num=1, boldness=1): | |
| """Overlay name text on page 2 of the PDF using image-based rendering.""" | |
| doc = fitz.open(stream=pdf_bytes, filetype="pdf") | |
| if page_num < 1 or page_num > len(doc): | |
| page_num = min(page_num, len(doc)) | |
| page = doc[page_num - 1] | |
| # ── Choose font file ──────────────────────────────────────────────────── | |
| # Use Gujarati font for Gujarati text; for English try system fonts. | |
| use_gujarati = is_gujarati(name_text) and os.path.exists(FONT_PATH) | |
| if use_gujarati: | |
| font_path = FONT_PATH | |
| else: | |
| # Common system font locations (Windows / Linux / macOS) | |
| candidates = [ | |
| os.path.join(os.path.dirname(__file__), "GujaratiFont.ttf"), | |
| "C:/Windows/Fonts/arial.ttf", | |
| "C:/Windows/Fonts/calibri.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", | |
| "/System/Library/Fonts/Supplemental/Arial.ttf", | |
| ] | |
| font_path = next((p for p in candidates if os.path.exists(p)), None) | |
| # ── Render text → PNG → embed in PDF ─────────────────────────────────── | |
| if font_path: | |
| try: | |
| img_bytes, img_w_px, img_h_px = render_text_as_image( | |
| name_text, font_path, font_size, color, boldness | |
| ) | |
| # Convert pixel size → PDF points (96 DPI screen → 72 pt) | |
| pdf_w = img_w_px * 72 / 96 | |
| pdf_h = img_h_px * 72 / 96 | |
| # pos_y is the text baseline; place image just above it | |
| rect = fitz.Rect(pos_x, pos_y - pdf_h, pos_x + pdf_w, pos_y) | |
| page.insert_image(rect, stream=img_bytes, keep_proportion=False) | |
| except Exception as img_err: | |
| # Last-resort fallback: plain insert_text (may not render Gujarati) | |
| page.insert_text( | |
| fitz.Point(pos_x, pos_y), name_text, | |
| fontname="helv", fontsize=font_size, color=color | |
| ) | |
| else: | |
| # No font found at all — use built-in helv | |
| page.insert_text( | |
| fitz.Point(pos_x, pos_y), name_text, | |
| fontname="helv", fontsize=font_size, color=color | |
| ) | |
| output = io.BytesIO() | |
| doc.save(output) | |
| doc.close() | |
| output.seek(0) | |
| return output.read() | |
| def render_page_as_image(pdf_bytes, page_num=2, zoom=2.0): | |
| """Render a PDF page as PNG image (base64).""" | |
| doc = fitz.open(stream=pdf_bytes, filetype="pdf") | |
| if page_num < 1 or page_num > len(doc): | |
| page_num = min(page_num, len(doc)) | |
| page = doc[page_num - 1] | |
| mat = fitz.Matrix(zoom, zoom) | |
| pix = page.get_pixmap(matrix=mat) | |
| img_bytes = pix.tobytes("png") | |
| doc.close() | |
| return base64.b64encode(img_bytes).decode() | |
| def index(): | |
| return render_template_string(HTML) | |
| def transliterate(): | |
| """Convert English phonetic text to Gujarati using Google Input Tools API.""" | |
| try: | |
| import urllib.request | |
| import urllib.parse | |
| import json as _json | |
| data = request.get_json() | |
| text = data.get('text', '').strip() | |
| if not text: | |
| return jsonify({'result': ''}) | |
| # Transliterate each word separately for better accuracy | |
| words = text.split() | |
| gujarati_words = [] | |
| for word in words: | |
| encoded = urllib.parse.quote(word) | |
| url = ( | |
| f"https://inputtools.google.com/request" | |
| f"?text={encoded}&itc=gu-t-i0-und&num=1&cp=0&cs=1&ie=utf-8&oe=utf-8" | |
| ) | |
| req = urllib.request.Request( | |
| url, | |
| headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} | |
| ) | |
| with urllib.request.urlopen(req, timeout=6) as resp: | |
| result = _json.loads(resp.read().decode('utf-8')) | |
| # Response format: ["SUCCESS", [["word", ["gu_word", ...], ...]], ...] | |
| if result and result[0] == 'SUCCESS' and result[1]: | |
| gu_word = result[1][0][1][0] | |
| gujarati_words.append(gu_word) | |
| else: | |
| gujarati_words.append(word) # keep original if conversion fails | |
| return jsonify({'result': ' '.join(gujarati_words)}) | |
| except Exception as e: | |
| return jsonify({'error': f'Transliteration failed: {str(e)}'}), 500 | |
| def preview(): | |
| try: | |
| data = request.get_json() | |
| pdf_b64 = data.get('pdf', '') | |
| name = data.get('name', '') | |
| font_size = int(data.get('fontSize', 18)) | |
| pos_x = float(data.get('posX', 165)) | |
| pos_y = float(data.get('posY', 340)) | |
| color = (data.get('colorR', 0.48), data.get('colorG', 0.10), data.get('colorB', 0.23)) | |
| page_num = int(data.get('page', 2)) | |
| boldness = int(data.get('boldness', 1)) | |
| pdf_bytes = base64.b64decode(pdf_b64) | |
| # Only overlay text on page 2 if viewing page 2 and name is provided | |
| if page_num == 2 and name.strip(): | |
| modified = process_pdf(pdf_bytes, name, font_size, pos_x, pos_y, color, page_num=2, boldness=boldness) | |
| else: | |
| modified = pdf_bytes | |
| img_b64 = render_page_as_image(modified, page_num=page_num, zoom=1.8) | |
| return jsonify({'image': img_b64}) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 400 | |
| def download(): | |
| try: | |
| data = request.get_json() | |
| pdf_b64 = data.get('pdf', '') | |
| name = data.get('name', '').strip() | |
| font_size = int(data.get('fontSize', 18)) | |
| pos_x = float(data.get('posX', 165)) | |
| pos_y = float(data.get('posY', 340)) | |
| color = (data.get('colorR', 0.48), data.get('colorG', 0.10), data.get('colorB', 0.23)) | |
| boldness = int(data.get('boldness', 1)) | |
| if not name: | |
| return jsonify({'error': 'Name is required'}), 400 | |
| pdf_bytes = base64.b64decode(pdf_b64) | |
| modified = process_pdf(pdf_bytes, name, font_size, pos_x, pos_y, color, page_num=2, boldness=boldness) | |
| # Filename from name | |
| safe_name = re.sub(r'[^\w\s\u0A80-\u0AFF]', '', name).strip().replace(' ', '_') or 'invitation' | |
| filename = safe_name + '.pdf' | |
| return send_file( | |
| io.BytesIO(modified), | |
| mimetype='application/pdf', | |
| as_attachment=True, | |
| download_name=filename | |
| ) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| # ── Locked PDF path ──────────────────────────────────────────────────────────── | |
| LOCKED_PDF_PATH = os.path.join(os.path.dirname(__file__), "locked_invitation.pdf") | |
| LOCKED_NAME_PATH = os.path.join(os.path.dirname(__file__), "locked_name.txt") | |
| def lock_pdf(): | |
| """Save the uploaded PDF permanently on the server.""" | |
| try: | |
| data = request.get_json() | |
| pdf_b64 = data.get('pdf', '') | |
| filename = data.get('filename', 'invitation.pdf') | |
| if not pdf_b64: | |
| return jsonify({'error': 'No PDF data'}), 400 | |
| pdf_bytes = base64.b64decode(pdf_b64) | |
| with open(LOCKED_PDF_PATH, 'wb') as f: | |
| f.write(pdf_bytes) | |
| with open(LOCKED_NAME_PATH, 'w', encoding='utf-8') as f: | |
| f.write(filename) | |
| return jsonify({'ok': True}) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| def get_locked_pdf(): | |
| """Return the locked PDF as base64 if it exists.""" | |
| if not os.path.exists(LOCKED_PDF_PATH): | |
| return jsonify({}), 404 | |
| with open(LOCKED_PDF_PATH, 'rb') as f: | |
| pdf_b64 = base64.b64encode(f.read()).decode() | |
| filename = 'invitation.pdf' | |
| if os.path.exists(LOCKED_NAME_PATH): | |
| with open(LOCKED_NAME_PATH, 'r', encoding='utf-8') as f: | |
| filename = f.read().strip() | |
| return jsonify({'pdf': pdf_b64, 'filename': filename}) | |
| def unlock_pdf(): | |
| """Delete the locked PDF from the server.""" | |
| try: | |
| if os.path.exists(LOCKED_PDF_PATH): | |
| os.remove(LOCKED_PDF_PATH) | |
| if os.path.exists(LOCKED_NAME_PATH): | |
| os.remove(LOCKED_NAME_PATH) | |
| return jsonify({'ok': True}) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| if __name__ == '__main__': | |
| print("\n" + "="*55) | |
| print(" ✉️ ચાંદલા વિધી PDF Editor") | |
| print("="*55) | |
| print(" 🌐 Open in browser:") | |
| print(" Local: http://localhost:5050") | |
| print(" Network: http://<your-ip>:5050") | |
| print("="*55) | |
| print(" 📱 Mobile: same URL on your phone (same WiFi)") | |
| print(" 📄 Gujarati font:", "✅ Lohit-Gujarati" if os.path.exists(FONT_PATH) else "⚠️ Not found") | |
| print("="*55 + "\n") | |
| app.run(host='0.0.0.0', port=5050, debug=False) |