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)