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 = """
PDF નામ ઉમેરો | Invitation Editor
✉️ ચાંદલા વિધી — આમંત્રણ સંપાદક
PDF invitation editor • Add recipient name to invitation
📄 Step 1: PDF પસંદ કરો
🔒 PDF લૉક છે — આ PDF આપોઆપ લોડ થઈ છે
📋 PDF લોડ થઈ ગઈ — pages
PDF lock કરો — દર વખતે upload નહીં કરવું પડે
✍️ Step 2: નામ ટાઇપ કરો
📁 filename.pdf
18
1
NormalLightMediumBoldExtra Bold
ઇન્વિટેશન ટેક્સ્ટ માટે ઘાટો રંગ સૂચવવામાં આવે છે
📍 ટેક્સ્ટ સ્થિતિ (Position)
💡 પ્રિવ્યૂ જોઈ X/Y બદલો જ્યાં "સ્નેહીશ્રી," ની પછી ખાલી જગ્યા છે
🎯 Step 3: Preview & Download
🖼️ PDF Preview
📄 Page 1 (Cover)
📝 Page 2 (Details)
📋
PDF અપલોડ કરો અને Preview બટન દબાવો
Page 2 પર "સ્નેહીશ્રી," ની પછી નામ ઉમેરાશે
ℹ️ Page 2 પર "સ્નેહીશ્રી,........" ની ખાલી જગ્યામાં નામ ઉમેરવામાં આવશે.
X=165, Y=340 ડિફૉલ્ટ position છે — preview જોઈ adjust કરો.
"""
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://: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)