DeepParmar commited on
Commit
a2d0f88
·
verified ·
1 Parent(s): 6601b13

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +18 -0
  2. GujaratiFont.ttf +0 -0
  3. README.md +77 -7
  4. app.py +1220 -0
  5. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ # Create a user with UID 1000 (Required for Hugging Face Spaces)
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ # Copy requirements and install
11
+ COPY --chown=user requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy all application files
15
+ COPY --chown=user . .
16
+
17
+ # Run the Flask app on Port 7860 using Gunicorn
18
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
GujaratiFont.ttf ADDED
Binary file (61.7 kB). View file
 
README.md CHANGED
@@ -1,10 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Huggingface
3
- emoji: 🐢
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # ✉️ ચાંદલા વિધી PDF Invitation Editor
2
+
3
+ A web-based app to add recipient names to Gujarati wedding invitation PDFs.
4
+
5
+ ---
6
+
7
+ ## 📋 ફાઇલ્સ (Files)
8
+
9
+ | File | Description |
10
+ |------|-------------|
11
+ | `app.py` | Main Flask application |
12
+ | `GujaratiFont.ttf` | Gujarati font (Lohit-Gujarati) |
13
+
14
+ ---
15
+
16
+ ## 🚀 Setup & Run
17
+
18
+ ### 1. Install Python packages
19
+ ```bash
20
+ pip install flask PyMuPDF
21
+ ```
22
+
23
+ ### 2. Place files together
24
+ ```
25
+ your-folder/
26
+ ├── app.py
27
+ └── GujaratiFont.ttf
28
+ ```
29
+
30
+ ### 3. Run the app
31
+ ```bash
32
+ python app.py
33
+ ```
34
+
35
+ ### 4. Open in browser
36
+ - **Computer:** http://localhost:5050
37
+ - **Mobile (same WiFi):** http://YOUR-PC-IP:5050
38
+
39
+ ---
40
+
41
+ ## 📱 How to Use
42
+
43
+ 1. **Upload** your PDF invitation file
44
+ 2. **Type** the recipient's name (Gujarati or English)
45
+ 3. **Adjust** font size (10–40)
46
+ 4. **Pick** text color
47
+ 5. **Set X/Y position** to place name on the dotted line
48
+ 6. Click **Preview** to see result on Page 2
49
+ 7. Click **Download** — PDF saves with the name as filename!
50
+
51
+ ---
52
+
53
+ ## 🎯 Position Tips
54
+
55
+ For the "સ્નેહીશ્રી,........" line:
56
+ - Default: **X=165, Y=340**
57
+ - If text appears too high → increase Y
58
+ - If text appears too far right → decrease X
59
+ - Use Preview button to check and adjust
60
+
61
  ---
62
+
63
+ ## 📝 Filename Logic
64
+
65
+ PDF is saved with the recipient name as filename:
66
+ - Input: `ભૂષણભાઈ પટેલ`
67
+ - Output: `ભૂષણભાઈ_પટેલ.pdf`
68
+
69
+ ---
70
+
71
+ ## 🔧 Anek Gujarati Font (Optional)
72
+
73
+ To use the exact Anek Gujarati font from the invitation:
74
+ 1. Download from: https://fonts.google.com/specimen/Anek+Gujarati
75
+ 2. Save as `AnekGujarati.ttf` in the same folder as `app.py`
76
+ 3. Edit `app.py` line: `FONT_PATH = "AnekGujarati.ttf"`
77
+
78
  ---
79
 
80
+ *Built with Flask + PyMuPDF*
app.py ADDED
@@ -0,0 +1,1220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import re
5
+ import fitz # PyMuPDF
6
+ from flask import Flask, render_template_string, request, send_file, jsonify
7
+
8
+ app = Flask(__name__)
9
+
10
+ # Font path - prefers Anek Gujarati if available, falls back to system font
11
+ FONT_PATH = os.path.join(os.path.dirname(__file__), "GujaratiFont.ttf")
12
+ UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "uploads")
13
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
14
+
15
+ HTML = """<!DOCTYPE html>
16
+ <html lang="mul">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>PDF નામ ઉમેરો | Invitation Editor</title>
21
+ <link rel="preconnect" href="https://fonts.googleapis.com">
22
+ <link href="https://fonts.googleapis.com/css2?family=Anek+Gujarati:wght@100..800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
23
+ <style>
24
+ :root {
25
+ --primary: #8B2252;
26
+ --primary-light: #c4547c;
27
+ --accent: #d4a843;
28
+ --bg: #fdf6ee;
29
+ --card: #ffffff;
30
+ --border: #e8d5c4;
31
+ --text: #3a2a1a;
32
+ --text-muted: #7a6a5a;
33
+ --shadow: 0 4px 24px rgba(139,34,82,0.10);
34
+ --radius: 16px;
35
+ }
36
+ * { box-sizing: border-box; margin: 0; padding: 0; }
37
+ body {
38
+ font-family: 'Inter', sans-serif;
39
+ background: var(--bg);
40
+ color: var(--text);
41
+ min-height: 100vh;
42
+ }
43
+
44
+ /* Header */
45
+ .header {
46
+ background: linear-gradient(135deg, var(--primary) 0%, #5a1236 100%);
47
+ color: white;
48
+ padding: 20px 24px;
49
+ text-align: center;
50
+ box-shadow: 0 2px 12px rgba(0,0,0,0.2);
51
+ }
52
+ .header h1 {
53
+ font-family: 'Anek Gujarati', sans-serif;
54
+ font-size: clamp(1.4rem, 4vw, 2rem);
55
+ font-weight: 700;
56
+ letter-spacing: 0.5px;
57
+ }
58
+ .header p {
59
+ font-size: 0.85rem;
60
+ opacity: 0.85;
61
+ margin-top: 4px;
62
+ }
63
+
64
+ /* Main layout */
65
+ .container {
66
+ max-width: 1100px;
67
+ margin: 0 auto;
68
+ padding: 24px 16px;
69
+ display: grid;
70
+ grid-template-columns: 1fr 1fr;
71
+ gap: 24px;
72
+ align-items: start;
73
+ }
74
+ @media (max-width: 700px) {
75
+ .container { grid-template-columns: 1fr; }
76
+ }
77
+
78
+ /* Cards */
79
+ .card {
80
+ background: var(--card);
81
+ border-radius: var(--radius);
82
+ border: 1px solid var(--border);
83
+ box-shadow: var(--shadow);
84
+ padding: 24px;
85
+ }
86
+ .card-title {
87
+ font-family: 'Anek Gujarati', sans-serif;
88
+ font-size: 1.1rem;
89
+ font-weight: 600;
90
+ color: var(--primary);
91
+ margin-bottom: 18px;
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ }
96
+ .card-title span { font-size: 1.3rem; }
97
+
98
+ /* Form elements */
99
+ .form-group { margin-bottom: 18px; }
100
+ label {
101
+ display: block;
102
+ font-size: 0.82rem;
103
+ font-weight: 600;
104
+ color: var(--text-muted);
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.6px;
107
+ margin-bottom: 6px;
108
+ }
109
+ label.gujarati-label {
110
+ font-family: 'Anek Gujarati', sans-serif;
111
+ font-size: 0.9rem;
112
+ text-transform: none;
113
+ letter-spacing: 0;
114
+ }
115
+ input[type="text"], input[type="file"], select {
116
+ width: 100%;
117
+ padding: 10px 14px;
118
+ border: 1.5px solid var(--border);
119
+ border-radius: 10px;
120
+ font-size: 1rem;
121
+ font-family: 'Anek Gujarati', 'Inter', sans-serif;
122
+ background: #fafafa;
123
+ color: var(--text);
124
+ transition: border-color 0.2s, box-shadow 0.2s;
125
+ outline: none;
126
+ }
127
+ input[type="text"]:focus, select:focus {
128
+ border-color: var(--primary);
129
+ box-shadow: 0 0 0 3px rgba(139,34,82,0.08);
130
+ background: #fff;
131
+ }
132
+ input[type="file"] {
133
+ padding: 8px 12px;
134
+ cursor: pointer;
135
+ font-size: 0.85rem;
136
+ }
137
+
138
+ /* Font size range */
139
+ .range-row {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 12px;
143
+ }
144
+ input[type="range"] {
145
+ flex: 1;
146
+ -webkit-appearance: none;
147
+ height: 6px;
148
+ border-radius: 3px;
149
+ background: linear-gradient(to right, var(--primary) 0%, var(--primary) var(--pct,50%), #e0d0d8 var(--pct,50%), #e0d0d8 100%);
150
+ outline: none;
151
+ }
152
+ input[type="range"]::-webkit-slider-thumb {
153
+ -webkit-appearance: none;
154
+ width: 20px; height: 20px;
155
+ border-radius: 50%;
156
+ background: var(--primary);
157
+ cursor: pointer;
158
+ box-shadow: 0 2px 6px rgba(139,34,82,0.3);
159
+ }
160
+ .range-value {
161
+ min-width: 36px;
162
+ text-align: center;
163
+ font-weight: 700;
164
+ color: var(--primary);
165
+ font-size: 1rem;
166
+ }
167
+
168
+ /* Position controls */
169
+ .position-grid {
170
+ display: grid;
171
+ grid-template-columns: 1fr 1fr;
172
+ gap: 12px;
173
+ }
174
+ .pos-group label { margin-bottom: 4px; }
175
+ .pos-group input[type="number"] {
176
+ width: 100%;
177
+ padding: 8px 12px;
178
+ border: 1.5px solid var(--border);
179
+ border-radius: 10px;
180
+ font-size: 0.95rem;
181
+ background: #fafafa;
182
+ color: var(--text);
183
+ outline: none;
184
+ transition: border-color 0.2s;
185
+ }
186
+ .pos-group input[type="number"]:focus {
187
+ border-color: var(--primary);
188
+ background: #fff;
189
+ }
190
+
191
+ /* Color picker row */
192
+ .color-row {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 10px;
196
+ }
197
+ input[type="color"] {
198
+ width: 44px;
199
+ height: 38px;
200
+ border: 1.5px solid var(--border);
201
+ border-radius: 8px;
202
+ cursor: pointer;
203
+ padding: 2px;
204
+ background: #fafafa;
205
+ }
206
+ .color-label { font-size: 0.85rem; color: var(--text-muted); }
207
+
208
+ /* Buttons */
209
+ .btn {
210
+ display: inline-flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ gap: 8px;
214
+ padding: 12px 22px;
215
+ border-radius: 10px;
216
+ font-weight: 600;
217
+ font-size: 0.95rem;
218
+ cursor: pointer;
219
+ border: none;
220
+ transition: all 0.2s;
221
+ font-family: 'Anek Gujarati', 'Inter', sans-serif;
222
+ width: 100%;
223
+ margin-bottom: 10px;
224
+ }
225
+ .btn-primary {
226
+ background: linear-gradient(135deg, var(--primary), #6a1040);
227
+ color: white;
228
+ box-shadow: 0 3px 12px rgba(139,34,82,0.25);
229
+ }
230
+ .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 5px 16px rgba(139,34,82,0.35); }
231
+ .btn-primary:active { transform: translateY(0); }
232
+ .btn-accent {
233
+ background: linear-gradient(135deg, var(--accent), #b8862a);
234
+ color: white;
235
+ box-shadow: 0 3px 12px rgba(212,168,67,0.3);
236
+ }
237
+ .btn-accent:hover { transform: translateY(-1px); }
238
+ .btn-outline {
239
+ background: white;
240
+ color: var(--primary);
241
+ border: 2px solid var(--primary);
242
+ }
243
+ .btn-outline:hover { background: var(--bg); }
244
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }
245
+
246
+ /* Preview area */
247
+ .preview-area {
248
+ background: #f0e8e0;
249
+ border-radius: 12px;
250
+ min-height: 300px;
251
+ display: flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ overflow: hidden;
255
+ position: relative;
256
+ margin-bottom: 16px;
257
+ border: 1.5px solid var(--border);
258
+ }
259
+ .preview-area img {
260
+ max-width: 100%;
261
+ max-height: 500px;
262
+ object-fit: contain;
263
+ border-radius: 8px;
264
+ display: block;
265
+ }
266
+ .preview-placeholder {
267
+ text-align: center;
268
+ color: var(--text-muted);
269
+ padding: 40px 20px;
270
+ }
271
+ .preview-placeholder .icon { font-size: 3rem; margin-bottom: 12px; }
272
+ .preview-placeholder p { font-size: 0.9rem; }
273
+
274
+ /* Page selector */
275
+ .page-tabs {
276
+ display: flex;
277
+ gap: 8px;
278
+ margin-bottom: 14px;
279
+ }
280
+ .page-tab {
281
+ flex: 1;
282
+ padding: 8px;
283
+ border: 1.5px solid var(--border);
284
+ border-radius: 8px;
285
+ background: white;
286
+ cursor: pointer;
287
+ font-size: 0.82rem;
288
+ font-weight: 600;
289
+ color: var(--text-muted);
290
+ text-align: center;
291
+ transition: all 0.2s;
292
+ }
293
+ .page-tab.active {
294
+ background: var(--primary);
295
+ color: white;
296
+ border-color: var(--primary);
297
+ }
298
+
299
+ /* Status / alerts */
300
+ .alert {
301
+ padding: 10px 14px;
302
+ border-radius: 8px;
303
+ font-size: 0.85rem;
304
+ margin-bottom: 14px;
305
+ display: none;
306
+ }
307
+ .alert.success { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
308
+ .alert.error { background: #fce4ec; color: #c62828; border: 1px solid #ef9a9a; }
309
+ .alert.info { background: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; }
310
+ .alert.show { display: block; }
311
+
312
+ /* Loading */
313
+ .spinner {
314
+ display: inline-block;
315
+ width: 18px; height: 18px;
316
+ border: 2.5px solid rgba(255,255,255,0.3);
317
+ border-top-color: white;
318
+ border-radius: 50%;
319
+ animation: spin 0.7s linear infinite;
320
+ }
321
+ @keyframes spin { to { transform: rotate(360deg); } }
322
+ .loading { display: none; }
323
+
324
+ /* Preview loading overlay */
325
+ .preview-loading {
326
+ position: absolute;
327
+ inset: 0;
328
+ background: rgba(255,255,255,0.85);
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ border-radius: 12px;
333
+ display: none;
334
+ }
335
+ .preview-loading.show { display: flex; }
336
+ .spinner-dark {
337
+ width: 32px; height: 32px;
338
+ border: 3px solid rgba(139,34,82,0.2);
339
+ border-top-color: var(--primary);
340
+ border-radius: 50%;
341
+ animation: spin 0.8s linear infinite;
342
+ }
343
+
344
+ /* Divider */
345
+ .divider {
346
+ border: none;
347
+ border-top: 1.5px solid var(--border);
348
+ margin: 18px 0;
349
+ }
350
+
351
+ /* Info badge */
352
+ .info-badge {
353
+ display: inline-flex;
354
+ align-items: center;
355
+ gap: 5px;
356
+ background: #fff3e0;
357
+ color: #e65100;
358
+ padding: 4px 10px;
359
+ border-radius: 20px;
360
+ font-size: 0.75rem;
361
+ font-weight: 600;
362
+ margin-bottom: 14px;
363
+ }
364
+
365
+ /* Filename preview */
366
+ .filename-preview {
367
+ background: #f5f0fb;
368
+ border: 1.5px dashed var(--primary-light);
369
+ border-radius: 8px;
370
+ padding: 8px 12px;
371
+ font-size: 0.82rem;
372
+ color: var(--primary);
373
+ font-weight: 600;
374
+ margin-top: 6px;
375
+ font-family: monospace;
376
+ }
377
+
378
+ /* Boldness scale */
379
+ .boldness-tag {
380
+ background: var(--primary);
381
+ color: white;
382
+ padding: 2px 10px;
383
+ border-radius: 20px;
384
+ font-size: 0.72rem;
385
+ font-weight: 600;
386
+ text-transform: uppercase;
387
+ letter-spacing: 0.5px;
388
+ vertical-align: middle;
389
+ }
390
+ .boldness-scale {
391
+ display: flex;
392
+ justify-content: space-between;
393
+ margin-top: 4px;
394
+ font-size: 0.68rem;
395
+ color: var(--text-muted);
396
+ }
397
+
398
+ /* Convert button */
399
+ .btn-convert {
400
+ display: inline-flex;
401
+ align-items: center;
402
+ gap: 6px;
403
+ margin-top: 8px;
404
+ padding: 8px 16px;
405
+ background: linear-gradient(135deg, #1a6b3c, #0f4d2a);
406
+ color: white;
407
+ border: none;
408
+ border-radius: 8px;
409
+ font-size: 0.88rem;
410
+ font-weight: 600;
411
+ cursor: pointer;
412
+ font-family: 'Anek Gujarati', 'Inter', sans-serif;
413
+ transition: all 0.2s;
414
+ box-shadow: 0 2px 8px rgba(26,107,60,0.25);
415
+ }
416
+ .btn-convert:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(26,107,60,0.35); }
417
+ .btn-convert:active { transform: translateY(0); }
418
+ .btn-convert:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
419
+
420
+ /* Lock PDF */
421
+ .lock-bar {
422
+ display: flex;
423
+ gap: 8px;
424
+ margin-top: 12px;
425
+ align-items: center;
426
+ flex-wrap: wrap;
427
+ }
428
+ .btn-lock {
429
+ display: inline-flex; align-items: center; gap: 6px;
430
+ padding: 8px 14px;
431
+ border: none; border-radius: 8px;
432
+ font-size: 0.82rem; font-weight: 600; cursor: pointer;
433
+ font-family: 'Anek Gujarati','Inter',sans-serif;
434
+ transition: all 0.2s;
435
+ }
436
+ .btn-lock-on { background: #1a3a6b; color: white; box-shadow: 0 2px 8px rgba(26,58,107,0.25); }
437
+ .btn-lock-on:hover { background: #142d54; }
438
+ .btn-lock-off { background: #b71c1c; color: white; box-shadow: 0 2px 8px rgba(183,28,28,0.25); }
439
+ .btn-lock-off:hover { background: #8b0000; }
440
+ .lock-status {
441
+ font-size: 0.78rem; color: var(--text-muted);
442
+ flex: 1; min-width: 120px;
443
+ }
444
+ .lock-status.locked { color: #1a6b3c; font-weight: 600; }
445
+ </style>
446
+ </head>
447
+ <body>
448
+
449
+ <div class="header">
450
+ <h1>✉️ ચાંદલા વિધી — આમંત્રણ સંપાદક</h1>
451
+ <p>PDF invitation editor • Add recipient name to invitation</p>
452
+ </div>
453
+
454
+ <div class="container">
455
+
456
+ <!-- LEFT: Settings Panel -->
457
+ <div>
458
+ <!-- Step 1: Upload PDF -->
459
+ <div class="card" style="margin-bottom:20px;">
460
+ <div class="card-title"><span>📄</span> Step 1: PDF પસંદ કરો</div>
461
+
462
+ <div id="lockedBanner" style="display:none;" class="alert success" style="margin-bottom:10px;">
463
+ 🔒 PDF લૉક છે — આ PDF આપોઆપ લોડ થઈ છે
464
+ </div>
465
+
466
+ <div class="form-group">
467
+ <label>આમંત્રણ PDF ફાઇલ અપલોડ કરો</label>
468
+ <input type="file" id="pdfFile" accept=".pdf" onchange="handlePDFUpload(this)">
469
+ </div>
470
+ <div id="uploadStatus" class="alert info">📋 PDF લોડ થઈ ગઈ — <span id="pageCount"></span> pages</div>
471
+
472
+ <div class="lock-bar">
473
+ <button class="btn-lock btn-lock-on" id="lockBtn" onclick="lockPDF()" title="Save this PDF permanently on server">
474
+ 🔒 Lock PDF
475
+ </button>
476
+ <button class="btn-lock btn-lock-off" id="unlockBtn" onclick="unlockPDF()" style="display:none;" title="Remove saved PDF">
477
+ 🔓 Unlock PDF
478
+ </button>
479
+ <span class="lock-status" id="lockStatus">PDF lock કરો — દર વખતે upload નહીં કરવું પડે</span>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- Step 2: Name Input -->
484
+ <div class="card" style="margin-bottom:20px;">
485
+ <div class="card-title"><span>✍️</span> Step 2: નામ ટાઇપ કરો</div>
486
+
487
+ <div class="form-group">
488
+ <label class="gujarati-label">સ્નેહીશ્રી, ______________ (નામ)</label>
489
+ <input type="text" id="nameText" lang="en"
490
+ placeholder="Type English name & click Convert → or type Gujarati directly"
491
+ autocomplete="off" spellcheck="false"
492
+ oninput="updateFilenamePreview(); autoPreview()">
493
+ <button class="btn-convert" onclick="convertToGujarati()" id="convertBtn" type="button">
494
+ <span id="convertSpinner" style="display:none;">⏳</span>
495
+ <span id="convertLabel">🔤→ ગુ Convert to Gujarati</span>
496
+ </button>
497
+ <div id="convertAlert" style="display:none; font-size:0.8rem; color:#c62828; margin-top:4px;"></div>
498
+ <div class="filename-preview" id="filenamePreview">📁 filename.pdf</div>
499
+ </div>
500
+
501
+ <hr class="divider">
502
+
503
+ <!-- Font Size -->
504
+ <div class="form-group">
505
+ <label>ફૉન્ટ સાઈઝ (Font Size)</label>
506
+ <div class="range-row">
507
+ <input type="range" id="fontSize" min="10" max="40" value="18"
508
+ oninput="updateFontSizeDisplay(); autoPreview()">
509
+ <div class="range-value" id="fontSizeValue">18</div>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Boldness -->
514
+ <div class="form-group">
515
+ <label>જાડાઈ (Boldness) &nbsp;<span id="boldnessLabel" class="boldness-tag">Normal</span></label>
516
+ <div class="range-row">
517
+ <input type="range" id="boldness" min="1" max="5" value="1"
518
+ oninput="updateBoldnessDisplay(); autoPreview()">
519
+ <div class="range-value" id="boldnessValue">1</div>
520
+ </div>
521
+ <div class="boldness-scale">
522
+ <span>Normal</span><span>Light</span><span>Medium</span><span>Bold</span><span>Extra Bold</span>
523
+ </div>
524
+ </div>
525
+
526
+ <!-- Text Color -->
527
+ <div class="form-group">
528
+ <label>ટેક્સ્ટ રંગ (Text Color)</label>
529
+ <div class="color-row">
530
+ <input type="color" id="textColor" value="#7a1a3a"
531
+ onchange="autoPreview()">
532
+ <span class="color-label">ઇન્વિટેશન ટેક્સ્ટ માટે ઘાટો રંગ સૂચવવામાં આવે છે</span>
533
+ </div>
534
+ </div>
535
+
536
+ <hr class="divider">
537
+
538
+ <!-- Position Controls -->
539
+ <div class="info-badge">📍 ટેક્સ્ટ સ્થિતિ (Position)</div>
540
+ <div class="position-grid">
541
+ <div class="pos-group">
542
+ <label>X (→ ડાબે-જમણે)</label>
543
+ <input type="number" id="posX" value="165" min="0" max="600"
544
+ oninput="autoPreview()">
545
+ </div>
546
+ <div class="pos-group">
547
+ <label>Y (↓ ઉપર-નીચે)</label>
548
+ <input type="number" id="posY" value="340" min="0" max="1000"
549
+ oninput="autoPreview()">
550
+ </div>
551
+ </div>
552
+ <p style="font-size:0.75rem;color:var(--text-muted);margin-top:6px;">
553
+ 💡 પ્રિવ્યૂ જોઈ X/Y બદલો જ્યાં "સ્નેહીશ્રી," ની પછી ખાલી જગ્યા છે
554
+ </p>
555
+ </div>
556
+
557
+ <!-- Step 3: Actions -->
558
+ <div class="card">
559
+ <div class="card-title"><span>🎯</span> Step 3: Preview & Download</div>
560
+
561
+ <div id="actionAlert" class="alert error"></div>
562
+
563
+ <button class="btn btn-primary" onclick="generatePreview()" id="previewBtn">
564
+ <span class="loading" id="previewLoad"><span class="spinner"></span></span>
565
+ 👁️ Preview જુઓ (Page 2)
566
+ </button>
567
+
568
+ <button class="btn btn-accent" onclick="downloadPDF()" id="downloadBtn">
569
+ <span class="loading" id="downloadLoad"><span class="spinner"></span></span>
570
+ ⬇️ PDF Download કરો
571
+ </button>
572
+
573
+ <button class="btn btn-outline" onclick="previewPage(1)" id="page1Btn">
574
+ 📋 Page 1 (Cover) જુઓ
575
+ </button>
576
+ </div>
577
+ </div>
578
+
579
+ <!-- RIGHT: Preview Panel -->
580
+ <div class="card">
581
+ <div class="card-title"><span>🖼️</span> PDF Preview</div>
582
+
583
+ <div class="page-tabs">
584
+ <div class="page-tab active" id="tab1" onclick="switchTab(1)">📄 Page 1 (Cover)</div>
585
+ <div class="page-tab" id="tab2" onclick="switchTab(2)">📝 Page 2 (Details)</div>
586
+ </div>
587
+
588
+ <div class="preview-area" id="previewArea">
589
+ <div class="preview-loading" id="previewLoading">
590
+ <div class="spinner-dark"></div>
591
+ </div>
592
+ <div class="preview-placeholder" id="previewPlaceholder">
593
+ <div class="icon">📋</div>
594
+ <p>PDF અપલોડ કરો અને<br>Preview બટન દબાવો</p>
595
+ <p style="margin-top:8px;font-size:0.78rem;opacity:0.6">
596
+ Page 2 પર "સ્નેહીશ્રી," ની<br>પછી નામ ઉમેરાશે
597
+ </p>
598
+ </div>
599
+ <img id="previewImg" style="display:none;" alt="PDF Preview">
600
+ </div>
601
+
602
+ <div class="alert info show" id="previewNote" style="font-size:0.8rem;">
603
+ ℹ️ <strong>Page 2</strong> પર "સ્નેહીશ્રી,........" ની ખાલી જગ્યામાં નામ ઉમેરવામાં આવશે.<br>
604
+ X=165, Y=340 ડિફૉલ્ટ position છે — preview જોઈ adjust કરો.
605
+ </div>
606
+ </div>
607
+
608
+ </div>
609
+
610
+ <script>
611
+ let pdfData = null;
612
+ let currentTab = 2;
613
+ let autoPreviewTimer = null;
614
+
615
+ // ── Font size display ──────────────────────────────────────
616
+ function updateFontSizeDisplay() {
617
+ const val = document.getElementById('fontSize').value;
618
+ document.getElementById('fontSizeValue').textContent = val;
619
+ // Update range gradient
620
+ const min = 10, max = 40;
621
+ const pct = ((val - min) / (max - min)) * 100;
622
+ document.getElementById('fontSize').style.setProperty('--pct', pct + '%');
623
+ }
624
+
625
+ // ── Filename preview ───────────────────────────────────────
626
+ function updateFilenamePreview() {
627
+ const name = document.getElementById('nameText').value.trim();
628
+ const safe = name.replace(/[^\\w\\s\\u0A80-\\u0AFF]/g, '').replace(/\\s+/g, '_') || 'invitation';
629
+ document.getElementById('filenamePreview').textContent = '📁 ' + safe + '.pdf';
630
+ }
631
+
632
+ // ── Convert English → Gujarati ─────────────────────────────
633
+ async function convertToGujarati() {
634
+ const input = document.getElementById('nameText');
635
+ const text = input.value.trim();
636
+ if (!text) {
637
+ showConvertAlert('⚠️ પ્રથમ English નામ ટાઇપ કરો!');
638
+ return;
639
+ }
640
+ // If already Gujarati, skip
641
+ if (/[\\u0A80-\\u0AFF]/.test(text)) {
642
+ showConvertAlert('ℹ️ ટેક્સ્ટ પહેલેથી ગુજરાતીમાં છે!');
643
+ return;
644
+ }
645
+
646
+ const btn = document.getElementById('convertBtn');
647
+ const spinner = document.getElementById('convertSpinner');
648
+ const label = document.getElementById('convertLabel');
649
+ btn.disabled = true;
650
+ spinner.style.display = 'inline';
651
+ label.textContent = 'Converting...';
652
+ hideConvertAlert();
653
+
654
+ try {
655
+ const resp = await fetch('/transliterate', {
656
+ method: 'POST',
657
+ headers: {'Content-Type': 'application/json'},
658
+ body: JSON.stringify({text})
659
+ });
660
+ const data = await resp.json();
661
+ if (data.error) {
662
+ showConvertAlert('❌ ' + data.error);
663
+ } else if (data.result) {
664
+ input.value = data.result;
665
+ updateFilenamePreview();
666
+ autoPreview();
667
+ } else {
668
+ showConvertAlert('❌ Conversion failed. Try typing Gujarati directly.');
669
+ }
670
+ } catch(e) {
671
+ showConvertAlert('❌ Network error: ' + e.message);
672
+ } finally {
673
+ btn.disabled = false;
674
+ spinner.style.display = 'none';
675
+ label.textContent = '🔤→ ગુ Convert to Gujarati';
676
+ }
677
+ }
678
+
679
+ function showConvertAlert(msg) {
680
+ const el = document.getElementById('convertAlert');
681
+ el.textContent = msg;
682
+ el.style.display = 'block';
683
+ setTimeout(() => el.style.display = 'none', 4000);
684
+ }
685
+ function hideConvertAlert() {
686
+ document.getElementById('convertAlert').style.display = 'none';
687
+ }
688
+
689
+ // ── PDF Upload ─────────────────────────────────────────────
690
+ function handlePDFUpload(input) {
691
+ const file = input.files[0];
692
+ if (!file) return;
693
+ const reader = new FileReader();
694
+ reader.onload = function(e) {
695
+ pdfData = e.target.result.split(',')[1]; // base64
696
+ document.getElementById('uploadStatus').className = 'alert info show';
697
+ document.getElementById('uploadStatus').innerHTML = '✅ PDF লোডেড: <strong>' + file.name + '</strong>';
698
+ // Auto preview page 2
699
+ setTimeout(() => generatePreview(), 300);
700
+ };
701
+ reader.readAsDataURL(file);
702
+ }
703
+
704
+ // ── Auto preview with debounce ─────────────────────────────
705
+ function autoPreview() {
706
+ if (!pdfData) return;
707
+ clearTimeout(autoPreviewTimer);
708
+ autoPreviewTimer = setTimeout(() => generatePreview(), 700);
709
+ }
710
+
711
+ // ── Switch page tab ────────────────────────────────────────
712
+ function switchTab(n) {
713
+ currentTab = n;
714
+ document.getElementById('tab1').classList.toggle('active', n === 1);
715
+ document.getElementById('tab2').classList.toggle('active', n === 2);
716
+ if (pdfData) generatePreview();
717
+ }
718
+
719
+ // ── Preview ────────────────────────────────────────────────
720
+ async function generatePreview() {
721
+ if (!pdfData) {
722
+ showAlert('actionAlert', 'error', '⚠️ પ્રથમ PDF ફાઇલ અપલોડ કરો!');
723
+ return;
724
+ }
725
+ hideAlert('actionAlert');
726
+ setLoading('previewLoad', true);
727
+ document.getElementById('previewLoading').classList.add('show');
728
+
729
+ try {
730
+ const params = getParams();
731
+ params.page = currentTab;
732
+ const resp = await fetch('/preview', {
733
+ method: 'POST',
734
+ headers: {'Content-Type': 'application/json'},
735
+ body: JSON.stringify(params)
736
+ });
737
+ const data = await resp.json();
738
+ if (data.error) {
739
+ showAlert('actionAlert', 'error', '❌ ' + data.error);
740
+ } else {
741
+ const img = document.getElementById('previewImg');
742
+ img.src = 'data:image/png;base64,' + data.image;
743
+ img.style.display = 'block';
744
+ document.getElementById('previewPlaceholder').style.display = 'none';
745
+ }
746
+ } catch(e) {
747
+ showAlert('actionAlert', 'error', '❌ Preview error: ' + e.message);
748
+ } finally {
749
+ setLoading('previewLoad', false);
750
+ document.getElementById('previewLoading').classList.remove('show');
751
+ }
752
+ }
753
+
754
+ async function previewPage(n) {
755
+ currentTab = n;
756
+ document.getElementById('tab1').classList.toggle('active', n===1);
757
+ document.getElementById('tab2').classList.toggle('active', n===2);
758
+ await generatePreview();
759
+ }
760
+
761
+ // ── Download ───────────────────────────────────────────────
762
+ async function downloadPDF() {
763
+ if (!pdfData) {
764
+ showAlert('actionAlert', 'error', '⚠️ પ્રથમ PDF ફાઇલ અપલોડ કરો!');
765
+ return;
766
+ }
767
+ const name = document.getElementById('nameText').value.trim();
768
+ if (!name) {
769
+ showAlert('actionAlert', 'error', '⚠️ કૃપા કરી નામ ટાઇપ કરો!');
770
+ return;
771
+ }
772
+
773
+ hideAlert('actionAlert');
774
+ setLoading('downloadLoad', true);
775
+
776
+ try {
777
+ const params = getParams();
778
+ const resp = await fetch('/download', {
779
+ method: 'POST',
780
+ headers: {'Content-Type': 'application/json'},
781
+ body: JSON.stringify(params)
782
+ });
783
+
784
+ if (!resp.ok) {
785
+ const data = await resp.json();
786
+ showAlert('actionAlert', 'error', '❌ ' + (data.error || 'Download failed'));
787
+ return;
788
+ }
789
+
790
+ const blob = await resp.blob();
791
+ const url = URL.createObjectURL(blob);
792
+ const a = document.createElement('a');
793
+ const safe = name.replace(/[^\\w\\s\\u0A80-\\u0AFF]/g, '').replace(/\\s+/g, '_') || 'invitation';
794
+ a.href = url;
795
+ a.download = safe + '.pdf';
796
+ a.click();
797
+ URL.revokeObjectURL(url);
798
+ showAlert('actionAlert', 'success', '✅ PDF Download થઈ ગઈ! (' + safe + '.pdf)');
799
+ } catch(e) {
800
+ showAlert('actionAlert', 'error', '❌ Download error: ' + e.message);
801
+ } finally {
802
+ setLoading('downloadLoad', false);
803
+ }
804
+ }
805
+
806
+ // ── Helpers ────────────────────────────────────────────────
807
+ function getParams() {
808
+ const color = document.getElementById('textColor').value;
809
+ const r = parseInt(color.slice(1,3),16)/255;
810
+ const g = parseInt(color.slice(3,5),16)/255;
811
+ const b = parseInt(color.slice(5,7),16)/255;
812
+ return {
813
+ pdf: pdfData,
814
+ name: document.getElementById('nameText').value,
815
+ fontSize: parseInt(document.getElementById('fontSize').value),
816
+ posX: parseInt(document.getElementById('posX').value),
817
+ posY: parseInt(document.getElementById('posY').value),
818
+ colorR: r, colorG: g, colorB: b,
819
+ boldness: parseInt(document.getElementById('boldness').value)
820
+ };
821
+ }
822
+
823
+ // ── Boldness display ───────────────────────────────────────
824
+ const boldnessNames = ['', 'Normal', 'Light Bold', 'Medium', 'Bold', 'Extra Bold'];
825
+ function updateBoldnessDisplay() {
826
+ const val = parseInt(document.getElementById('boldness').value);
827
+ document.getElementById('boldnessValue').textContent = val;
828
+ document.getElementById('boldnessLabel').textContent = boldnessNames[val];
829
+ const min = 1, max = 5;
830
+ const pct = ((val - min) / (max - min)) * 100;
831
+ document.getElementById('boldness').style.setProperty('--pct', pct + '%');
832
+ }
833
+
834
+ // ── Lock PDF ───────────────────────────────────────────────
835
+ async function lockPDF() {
836
+ if (!pdfData) {
837
+ alert('⚠️ પ્રથમ PDF upload કરો!');
838
+ return;
839
+ }
840
+ try {
841
+ const resp = await fetch('/lock_pdf', {
842
+ method: 'POST',
843
+ headers: {'Content-Type': 'application/json'},
844
+ body: JSON.stringify({pdf: pdfData})
845
+ });
846
+ const data = await resp.json();
847
+ if (data.ok) {
848
+ setLockUI(true);
849
+ } else {
850
+ alert('❌ Lock failed: ' + (data.error || 'Unknown error'));
851
+ }
852
+ } catch(e) {
853
+ alert('❌ ' + e.message);
854
+ }
855
+ }
856
+
857
+ async function unlockPDF() {
858
+ try {
859
+ const resp = await fetch('/unlock_pdf', {method: 'POST'});
860
+ const data = await resp.json();
861
+ if (data.ok) setLockUI(false);
862
+ } catch(e) {
863
+ alert('❌ ' + e.message);
864
+ }
865
+ }
866
+
867
+ function setLockUI(locked) {
868
+ document.getElementById('lockBtn').style.display = locked ? 'none' : 'inline-flex';
869
+ document.getElementById('unlockBtn').style.display = locked ? 'inline-flex' : 'none';
870
+ document.getElementById('lockStatus').textContent = locked
871
+ ? '✅ PDF સર્વર પર save છે — browser બંધ કરો, ફરી ખોલો, PDF હશે!'
872
+ : 'PDF lock કરો — દર વખતે upload નહીં કરવું પડે';
873
+ document.getElementById('lockStatus').className = 'lock-status' + (locked ? ' locked' : '');
874
+ document.getElementById('lockedBanner').style.display = locked ? 'block' : 'none';
875
+ }
876
+
877
+ // ── Auto-load locked PDF on page start ────────────────────
878
+ async function checkLockedPDF() {
879
+ try {
880
+ const resp = await fetch('/get_locked_pdf');
881
+ if (!resp.ok) return;
882
+ const data = await resp.json();
883
+ if (data.pdf) {
884
+ pdfData = data.pdf;
885
+ document.getElementById('uploadStatus').className = 'alert info show';
886
+ document.getElementById('uploadStatus').innerHTML =
887
+ '🔒 Locked PDF આપોઆપ લોડ થઈ: <strong>' + (data.filename || 'invitation.pdf') + '</strong>';
888
+ setLockUI(true);
889
+ setTimeout(() => generatePreview(), 400);
890
+ }
891
+ } catch(e) { /* no locked PDF, that's fine */ }
892
+ }
893
+
894
+ function setLoading(id, on) {
895
+ document.getElementById(id).style.display = on ? 'inline-block' : 'none';
896
+ }
897
+ function showAlert(id, type, msg) {
898
+ const el = document.getElementById(id);
899
+ el.className = 'alert ' + type + ' show';
900
+ el.textContent = msg;
901
+ }
902
+ function hideAlert(id) {
903
+ document.getElementById(id).className = 'alert';
904
+ }
905
+
906
+ // Init
907
+ updateFontSizeDisplay();
908
+ updateBoldnessDisplay();
909
+ updateFilenamePreview();
910
+ document.getElementById('fontSize').addEventListener('input', updateFontSizeDisplay);
911
+ document.getElementById('boldness').addEventListener('input', updateBoldnessDisplay);
912
+ checkLockedPDF();
913
+ </script>
914
+ </body>
915
+ </html>"""
916
+
917
+
918
+ def hex_to_rgb_float(hex_color):
919
+ hex_color = hex_color.lstrip('#')
920
+ return tuple(int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4))
921
+
922
+
923
+ def is_gujarati(text):
924
+ """Return True if text contains any Gujarati Unicode characters (U+0A80–U+0AFF)."""
925
+ return any('\u0A80' <= ch <= '\u0AFF' for ch in text)
926
+
927
+
928
+ def render_text_as_image(text, font_path, font_size_pt, color_float, boldness=1):
929
+ """
930
+ Render text to a transparent PNG using Pillow+FreeType (handles Gujarati shaping).
931
+ Returns (png_bytes, img_w_px, img_h_px).
932
+ """
933
+ from PIL import Image, ImageDraw, ImageFont
934
+
935
+ # Convert PDF points → screen pixels (96 DPI)
936
+ font_size_px = max(8, int(font_size_pt * 96 / 72))
937
+ color_rgba = tuple(int(c * 255) for c in color_float) + (255,)
938
+
939
+ try:
940
+ font = ImageFont.truetype(font_path, font_size_px)
941
+ except Exception:
942
+ font = ImageFont.load_default()
943
+
944
+ # Measure exact text bounding box
945
+ probe = Image.new('RGBA', (4000, 400), (0, 0, 0, 0))
946
+ draw = ImageDraw.Draw(probe)
947
+ bbox = draw.textbbox((0, 0), text, font=font) # (left, top, right, bottom)
948
+
949
+ text_w = max(bbox[2] - bbox[0], 1)
950
+ text_h = max(bbox[3] - bbox[1], 1)
951
+ pad = 6
952
+ extra = boldness - 1 # extra horizontal pixels for simulated bold
953
+
954
+ img_w = text_w + pad * 2 + extra + 4
955
+ img_h = text_h + pad * 2
956
+
957
+ img = Image.new('RGBA', (img_w, img_h), (0, 0, 0, 0))
958
+ draw = ImageDraw.Draw(img)
959
+
960
+ # Draw text `boldness` times with tiny horizontal offsets to simulate weight
961
+ step = 0.5
962
+ for i in range(boldness):
963
+ draw.text(
964
+ (pad - bbox[0] + i * step, pad - bbox[1]),
965
+ text, font=font, fill=color_rgba
966
+ )
967
+
968
+ buf = io.BytesIO()
969
+ img.save(buf, format='PNG')
970
+ buf.seek(0)
971
+ return buf.getvalue(), img_w, img_h
972
+
973
+
974
+ def process_pdf(pdf_bytes, name_text, font_size=18, pos_x=165, pos_y=340,
975
+ color=(0.48, 0.10, 0.23), page_num=1, boldness=1):
976
+ """Overlay name text on page 2 of the PDF using image-based rendering."""
977
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
978
+
979
+ if page_num < 1 or page_num > len(doc):
980
+ page_num = min(page_num, len(doc))
981
+
982
+ page = doc[page_num - 1]
983
+
984
+ # ── Choose font file ────────────────────────────────────────────────────
985
+ # Use Gujarati font for Gujarati text; for English try system fonts.
986
+ use_gujarati = is_gujarati(name_text) and os.path.exists(FONT_PATH)
987
+
988
+ if use_gujarati:
989
+ font_path = FONT_PATH
990
+ else:
991
+ # Common system font locations (Windows / Linux / macOS)
992
+ candidates = [
993
+ os.path.join(os.path.dirname(__file__), "GujaratiFont.ttf"),
994
+ "C:/Windows/Fonts/arial.ttf",
995
+ "C:/Windows/Fonts/calibri.ttf",
996
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
997
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
998
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
999
+ ]
1000
+ font_path = next((p for p in candidates if os.path.exists(p)), None)
1001
+
1002
+ # ── Render text → PNG → embed in PDF ───────────────────────────────────
1003
+ if font_path:
1004
+ try:
1005
+ img_bytes, img_w_px, img_h_px = render_text_as_image(
1006
+ name_text, font_path, font_size, color, boldness
1007
+ )
1008
+ # Convert pixel size → PDF points (96 DPI screen → 72 pt)
1009
+ pdf_w = img_w_px * 72 / 96
1010
+ pdf_h = img_h_px * 72 / 96
1011
+
1012
+ # pos_y is the text baseline; place image just above it
1013
+ rect = fitz.Rect(pos_x, pos_y - pdf_h, pos_x + pdf_w, pos_y)
1014
+ page.insert_image(rect, stream=img_bytes, keep_proportion=False)
1015
+
1016
+ except Exception as img_err:
1017
+ # Last-resort fallback: plain insert_text (may not render Gujarati)
1018
+ page.insert_text(
1019
+ fitz.Point(pos_x, pos_y), name_text,
1020
+ fontname="helv", fontsize=font_size, color=color
1021
+ )
1022
+ else:
1023
+ # No font found at all — use built-in helv
1024
+ page.insert_text(
1025
+ fitz.Point(pos_x, pos_y), name_text,
1026
+ fontname="helv", fontsize=font_size, color=color
1027
+ )
1028
+
1029
+ output = io.BytesIO()
1030
+ doc.save(output)
1031
+ doc.close()
1032
+ output.seek(0)
1033
+ return output.read()
1034
+
1035
+
1036
+ def render_page_as_image(pdf_bytes, page_num=2, zoom=2.0):
1037
+ """Render a PDF page as PNG image (base64)."""
1038
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
1039
+ if page_num < 1 or page_num > len(doc):
1040
+ page_num = min(page_num, len(doc))
1041
+ page = doc[page_num - 1]
1042
+ mat = fitz.Matrix(zoom, zoom)
1043
+ pix = page.get_pixmap(matrix=mat)
1044
+ img_bytes = pix.tobytes("png")
1045
+ doc.close()
1046
+ return base64.b64encode(img_bytes).decode()
1047
+
1048
+
1049
+ @app.route('/')
1050
+ def index():
1051
+ return render_template_string(HTML)
1052
+
1053
+
1054
+ @app.route('/transliterate', methods=['POST'])
1055
+ def transliterate():
1056
+ """Convert English phonetic text to Gujarati using Google Input Tools API."""
1057
+ try:
1058
+ import urllib.request
1059
+ import urllib.parse
1060
+ import json as _json
1061
+
1062
+ data = request.get_json()
1063
+ text = data.get('text', '').strip()
1064
+ if not text:
1065
+ return jsonify({'result': ''})
1066
+
1067
+ # Transliterate each word separately for better accuracy
1068
+ words = text.split()
1069
+ gujarati_words = []
1070
+
1071
+ for word in words:
1072
+ encoded = urllib.parse.quote(word)
1073
+ url = (
1074
+ f"https://inputtools.google.com/request"
1075
+ f"?text={encoded}&itc=gu-t-i0-und&num=1&cp=0&cs=1&ie=utf-8&oe=utf-8"
1076
+ )
1077
+ req = urllib.request.Request(
1078
+ url,
1079
+ headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
1080
+ )
1081
+ with urllib.request.urlopen(req, timeout=6) as resp:
1082
+ result = _json.loads(resp.read().decode('utf-8'))
1083
+
1084
+ # Response format: ["SUCCESS", [["word", ["gu_word", ...], ...]], ...]
1085
+ if result and result[0] == 'SUCCESS' and result[1]:
1086
+ gu_word = result[1][0][1][0]
1087
+ gujarati_words.append(gu_word)
1088
+ else:
1089
+ gujarati_words.append(word) # keep original if conversion fails
1090
+
1091
+ return jsonify({'result': ' '.join(gujarati_words)})
1092
+
1093
+ except Exception as e:
1094
+ return jsonify({'error': f'Transliteration failed: {str(e)}'}), 500
1095
+
1096
+
1097
+ @app.route('/preview', methods=['POST'])
1098
+ def preview():
1099
+ try:
1100
+ data = request.get_json()
1101
+ pdf_b64 = data.get('pdf', '')
1102
+ name = data.get('name', '')
1103
+ font_size = int(data.get('fontSize', 18))
1104
+ pos_x = float(data.get('posX', 165))
1105
+ pos_y = float(data.get('posY', 340))
1106
+ color = (data.get('colorR', 0.48), data.get('colorG', 0.10), data.get('colorB', 0.23))
1107
+ page_num = int(data.get('page', 2))
1108
+ boldness = int(data.get('boldness', 1))
1109
+
1110
+ pdf_bytes = base64.b64decode(pdf_b64)
1111
+
1112
+ # Only overlay text on page 2 if viewing page 2 and name is provided
1113
+ if page_num == 2 and name.strip():
1114
+ modified = process_pdf(pdf_bytes, name, font_size, pos_x, pos_y, color, page_num=2, boldness=boldness)
1115
+ else:
1116
+ modified = pdf_bytes
1117
+
1118
+ img_b64 = render_page_as_image(modified, page_num=page_num, zoom=1.8)
1119
+ return jsonify({'image': img_b64})
1120
+
1121
+ except Exception as e:
1122
+ return jsonify({'error': str(e)}), 400
1123
+
1124
+
1125
+ @app.route('/download', methods=['POST'])
1126
+ def download():
1127
+ try:
1128
+ data = request.get_json()
1129
+ pdf_b64 = data.get('pdf', '')
1130
+ name = data.get('name', '').strip()
1131
+ font_size = int(data.get('fontSize', 18))
1132
+ pos_x = float(data.get('posX', 165))
1133
+ pos_y = float(data.get('posY', 340))
1134
+ color = (data.get('colorR', 0.48), data.get('colorG', 0.10), data.get('colorB', 0.23))
1135
+ boldness = int(data.get('boldness', 1))
1136
+
1137
+ if not name:
1138
+ return jsonify({'error': 'Name is required'}), 400
1139
+
1140
+ pdf_bytes = base64.b64decode(pdf_b64)
1141
+ modified = process_pdf(pdf_bytes, name, font_size, pos_x, pos_y, color, page_num=2, boldness=boldness)
1142
+
1143
+ # Filename from name
1144
+ safe_name = re.sub(r'[^\w\s\u0A80-\u0AFF]', '', name).strip().replace(' ', '_') or 'invitation'
1145
+ filename = safe_name + '.pdf'
1146
+
1147
+ return send_file(
1148
+ io.BytesIO(modified),
1149
+ mimetype='application/pdf',
1150
+ as_attachment=True,
1151
+ download_name=filename
1152
+ )
1153
+
1154
+ except Exception as e:
1155
+ return jsonify({'error': str(e)}), 500
1156
+
1157
+
1158
+ # ── Locked PDF path ────────────────────────────────────────────────────────────
1159
+ LOCKED_PDF_PATH = os.path.join(os.path.dirname(__file__), "locked_invitation.pdf")
1160
+ LOCKED_NAME_PATH = os.path.join(os.path.dirname(__file__), "locked_name.txt")
1161
+
1162
+
1163
+ @app.route('/lock_pdf', methods=['POST'])
1164
+ def lock_pdf():
1165
+ """Save the uploaded PDF permanently on the server."""
1166
+ try:
1167
+ data = request.get_json()
1168
+ pdf_b64 = data.get('pdf', '')
1169
+ filename = data.get('filename', 'invitation.pdf')
1170
+ if not pdf_b64:
1171
+ return jsonify({'error': 'No PDF data'}), 400
1172
+ pdf_bytes = base64.b64decode(pdf_b64)
1173
+ with open(LOCKED_PDF_PATH, 'wb') as f:
1174
+ f.write(pdf_bytes)
1175
+ with open(LOCKED_NAME_PATH, 'w', encoding='utf-8') as f:
1176
+ f.write(filename)
1177
+ return jsonify({'ok': True})
1178
+ except Exception as e:
1179
+ return jsonify({'error': str(e)}), 500
1180
+
1181
+
1182
+ @app.route('/get_locked_pdf', methods=['GET'])
1183
+ def get_locked_pdf():
1184
+ """Return the locked PDF as base64 if it exists."""
1185
+ if not os.path.exists(LOCKED_PDF_PATH):
1186
+ return jsonify({}), 404
1187
+ with open(LOCKED_PDF_PATH, 'rb') as f:
1188
+ pdf_b64 = base64.b64encode(f.read()).decode()
1189
+ filename = 'invitation.pdf'
1190
+ if os.path.exists(LOCKED_NAME_PATH):
1191
+ with open(LOCKED_NAME_PATH, 'r', encoding='utf-8') as f:
1192
+ filename = f.read().strip()
1193
+ return jsonify({'pdf': pdf_b64, 'filename': filename})
1194
+
1195
+
1196
+ @app.route('/unlock_pdf', methods=['POST'])
1197
+ def unlock_pdf():
1198
+ """Delete the locked PDF from the server."""
1199
+ try:
1200
+ if os.path.exists(LOCKED_PDF_PATH):
1201
+ os.remove(LOCKED_PDF_PATH)
1202
+ if os.path.exists(LOCKED_NAME_PATH):
1203
+ os.remove(LOCKED_NAME_PATH)
1204
+ return jsonify({'ok': True})
1205
+ except Exception as e:
1206
+ return jsonify({'error': str(e)}), 500
1207
+
1208
+
1209
+ if __name__ == '__main__':
1210
+ print("\n" + "="*55)
1211
+ print(" ✉️ ચાંદલા વિધી PDF Editor")
1212
+ print("="*55)
1213
+ print(" 🌐 Open in browser:")
1214
+ print(" Local: http://localhost:5050")
1215
+ print(" Network: http://<your-ip>:5050")
1216
+ print("="*55)
1217
+ print(" 📱 Mobile: same URL on your phone (same WiFi)")
1218
+ print(" 📄 Gujarati font:", "✅ Lohit-Gujarati" if os.path.exists(FONT_PATH) else "⚠️ Not found")
1219
+ print("="*55 + "\n")
1220
+ app.run(host='0.0.0.0', port=5050, debug=False)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask==3.1.0
2
+ PyMuPDF==1.25.3
3
+ gunicorn==23.0.0