Huggingface / app.py
DeepParmar's picture
Upload 5 files
a2d0f88 verified
Raw History Blame Contribute Delete
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) &nbsp;<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() @app.route('/') def index(): return render_template_string(HTML) @app.route('/transliterate', methods=['POST']) 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 @app.route('/preview', methods=['POST']) 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 @app.route('/download', methods=['POST']) 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") @app.route('/lock_pdf', methods=['POST']) 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 @app.route('/get_locked_pdf', methods=['GET']) 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}) @app.route('/unlock_pdf', methods=['POST']) 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)
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 {