import streamlit as st from PIL import Image, ImageDraw import os import io from backend.text_editor.pdf_processor import get_pdf_page_count, pdf_page_to_pil, compile_images_to_pdf from backend.text_editor.ocr_engine import EASYOCR_AVAILABLE from backend.text_editor.orchestrator import TextEditorOrchestrator from backend.text_editor.font_detector import load_matching_font def load_demo_invoice(): """Create a beautiful synthetic demo invoice image to test instantly.""" w, h = 700, 900 img = Image.new("RGB", (w, h), "#FAFAFA") draw = ImageDraw.Draw(img) draw.rectangle([20, 20, w-20, h-20], fill="#FFFFFF", outline="#E0E0E0", width=2) try: from PIL import ImageFont font_large = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", 36) font_med = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", 16) font_bold = ImageFont.truetype("C:\\Windows\\Fonts\\arialbd.ttf", 16) except Exception: font_large = ImageFont.load_default() font_med = ImageFont.load_default() font_bold = ImageFont.load_default() draw.text((50, 60), "INVOICE", fill="#1A1B35", font=font_large) draw.text((50, 140), "Invoice Number: INV-2026-9042", fill="#555555", font=font_med) draw.text((50, 170), "Date: May 29, 2026", fill="#555555", font=font_med) draw.text((50, 200), "Billed To: Antigravity User", fill="#555555", font=font_med) draw.line([50, 240, w-50, 240], fill="#E0E0E0", width=1) draw.text((50, 260), "Description", fill="#1A1B35", font=font_bold) draw.text((450, 260), "Quantity", fill="#1A1B35", font=font_bold) draw.text((550, 260), "Amount", fill="#1A1B35", font=font_bold) draw.line([50, 290, w-50, 290], fill="#1A1B35", width=2) draw.text((50, 310), "AI Background Remover Software Licence", fill="#555555", font=font_med) draw.text((450, 310), "1", fill="#555555", font=font_med) draw.text((550, 310), "$149.00", fill="#555555", font=font_med) draw.text((50, 350), "Advanced Guided Filter Matting Module", fill="#555555", font=font_med) draw.text((450, 350), "2", fill="#555555", font=font_med) draw.text((550, 350), "$199.00", fill="#555555", font=font_med) draw.line([50, 420, w-50, 420], fill="#E0E0E0", width=1) draw.text((400, 450), "Subtotal:", fill="#555555", font=font_med) draw.text((550, 450), "$348.00", fill="#555555", font=font_med) draw.text((400, 480), "Tax (10%):", fill="#555555", font=font_med) draw.text((550, 480), "$34.80", fill="#555555", font=font_med) draw.text((400, 520), "Total Due:", fill="#1A1B35", font=font_bold) draw.text((550, 520), "$382.80", fill="#1A1B35", font=font_bold) draw.rectangle([50, 780, w-50, 840], fill="#F0F4F8", outline="#D0DCE5") draw.text((70, 795), "Thank you for your business! For queries contact support@antigravity.ai", fill="#4A5D6E", font=font_med) st.session_state.te_original_file = img st.session_state.te_file_type = "image" st.session_state.te_pdf_filename = "demo_invoice.png" st.session_state.te_original_image = img st.session_state.te_pdf_pages = [img] st.session_state.te_edited_image = None st.session_state.te_replacements = [] st.session_state.te_ocr_results = None def draw_ocr_overlays_with_badges(pil_img: Image.Image, ocr_results: list) -> Image.Image: """Overlays high-quality translucent bounding boxes and numbered circular badges over text areas.""" overlay_img = pil_img.copy() draw = ImageDraw.Draw(overlay_img, "RGBA") try: from PIL import ImageFont font_badge = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", 13) except Exception: font_badge = ImageFont.load_default() for idx, item in enumerate(ocr_results): x, y, w, h = item["bbox"] # Draw translucent cyan bounding box draw.rectangle([x, y, x + w, y + h], fill=(0, 240, 255, 15), outline=(0, 240, 255, 160), width=1) # Draw small solid pink badge at top-left corner of bounding box bx, by = x - 5, y - 5 br = 9 # radius draw.ellipse([bx - br, by - br, bx + br, by + br], fill=(255, 0, 127, 240)) # Center number in circle num_str = str(idx + 1) draw.text((bx - 4, by - 6), num_str, fill=(255, 255, 255, 255), font=font_badge) return overlay_img def generate_font_preview_pil(text: str, font_name: str) -> Image.Image: """Generates a small visual PIL image showing the replacement text rendered in the Google Font.""" w, h = 280, 50 canvas = Image.new("RGB", (w, h), "#F8F9FA") draw = ImageDraw.Draw(canvas) try: font, font_size = load_matching_font(bbox_height=26, font_family=font_name, size_multiplier=1.0, text=text) draw.text((10, 12), text, fill=(26, 27, 53), font=font) except Exception: draw.text((10, 15), text, fill=(26, 27, 53)) return canvas def render_text_editor_ui(): """Renders the modular AI In-Image & PDF Document Text Editor workspace with interactive numbered badges.""" st.markdown( """

📝 AI Document & PDF Text Editor

Correct text directly using visual numbered badges. Search and preview the most matching Google Fonts.

""", unsafe_allow_html=True ) # ----------------- SESSION STATE INITIALIZATION ----------------- if "te_original_file" not in st.session_state: st.session_state.te_original_file = None if "te_file_type" not in st.session_state: st.session_state.te_file_type = None if "te_pdf_filename" not in st.session_state: st.session_state.te_pdf_filename = "document" if "te_original_image" not in st.session_state: st.session_state.te_original_image = None if "te_pdf_pages" not in st.session_state: st.session_state.te_pdf_pages = [] if "te_active_page" not in st.session_state: st.session_state.te_active_page = 0 if "te_ocr_results" not in st.session_state: st.session_state.te_ocr_results = None if "te_edited_image" not in st.session_state: st.session_state.te_edited_image = None if "te_replacements" not in st.session_state: st.session_state.te_replacements = [] # ----------------- SIDEBAR CONTROLS ----------------- st.sidebar.markdown("### ⚙️ TEXT EDITOR CONTROLS") font_family = st.sidebar.selectbox( "Default Font Family", ["Sans-Serif", "Serif", "Monospace"], index=0, help="Selects the nearest matching system TTF font style for text rendering." ) custom_font = st.sidebar.text_input( "⚡ Or Search Google Font", value="", placeholder="e.g. Rozha One, Kalam, Hind, Poppins", help="Type any Google Font family name. The engine will dynamically download, extract, and cache the full font set!" ) if custom_font.strip(): font_family = custom_font.strip() size_multiplier = st.sidebar.slider( "Font Size Scale Adjust", min_value=0.5, max_value=1.5, value=0.90, step=0.05, help="Scale multiplier to perfectly size the drawn characters to the surrounding bounding box." ) overlay_ocr = st.sidebar.checkbox( "Overlay Numbered Badges", value=True, help="Draws glowing pink circular badges and cyan boxes over text blocks to make editing easy." ) # ----------------- MAIN WORKFLOW ----------------- # Case 1: No file loaded yet if st.session_state.te_original_file is None: col_l, col_c, col_r = st.columns([1, 3, 1]) with col_c: st.markdown( """

⚡ UPLOAD SOURCE DOCUMENT

Upload an Image or multi-page PDF Document. Coordinates and fonts will be parsed instantly.

""", unsafe_allow_html=True ) uploaded_file = st.file_uploader( "Choose document or image...", type=["pdf", "jpg", "jpeg", "png", "webp"], label_visibility="collapsed", key="doc_uploader" ) if uploaded_file is not None: try: file_bytes = uploaded_file.read() filename = uploaded_file.name st.session_state.te_pdf_filename = filename if filename.lower().endswith(".pdf"): st.session_state.te_original_file = file_bytes st.session_state.te_file_type = "pdf" page_count = get_pdf_page_count(file_bytes) with st.spinner(f"Extracting {page_count} PDF pages at high quality (150 DPI)..."): pdf_pages = [] for idx in range(page_count): pdf_pages.append(pdf_page_to_pil(file_bytes, idx, dpi=150)) st.session_state.te_pdf_pages = pdf_pages st.session_state.te_active_page = 0 st.session_state.te_original_image = pdf_pages[0] else: from PIL import ImageOps pil_img = Image.open(io.BytesIO(file_bytes)).convert("RGB") pil_img = ImageOps.exif_transpose(pil_img) st.session_state.te_original_file = file_bytes st.session_state.te_file_type = "image" st.session_state.te_pdf_pages = [pil_img] st.session_state.te_active_page = 0 st.session_state.te_original_image = pil_img st.session_state.te_edited_image = None st.session_state.te_replacements = [] st.session_state.te_ocr_results = None st.rerun() except Exception as e: st.error(f"Error loading document: {str(e)}") st.markdown("

No files ready? Test instantly with a pre-configured demo invoice:

", unsafe_allow_html=True) col_demo_left, col_demo_center, col_demo_right = st.columns([1, 1, 1]) with col_demo_center: if st.button("Load Demo Invoice", use_container_width=True): load_demo_invoice() st.rerun() # Case 2: Document loaded else: col_nav, col_reset = st.columns([5, 1]) with col_reset: if st.button("Reset / Clear", key="te_reset", use_container_width=True): st.session_state.te_original_file = None st.session_state.te_file_type = None st.session_state.te_pdf_filename = "document" st.session_state.te_original_image = None st.session_state.te_pdf_pages = [] st.session_state.te_ocr_results = None st.session_state.te_edited_image = None st.session_state.te_replacements = [] st.rerun() with col_nav: if st.session_state.te_file_type == "pdf": pages_count = len(st.session_state.te_pdf_pages) st.markdown( f"""
📄 File: {st.session_state.te_pdf_filename} | Total Pages: {pages_count}
""", unsafe_allow_html=True ) if pages_count > 1: active_pg = st.slider("Navigate Pages", 1, pages_count, st.session_state.te_active_page + 1) - 1 if active_pg != st.session_state.te_active_page: st.session_state.te_active_page = active_pg st.session_state.te_original_image = st.session_state.te_pdf_pages[active_pg] st.session_state.te_ocr_results = None st.session_state.te_edited_image = None st.session_state.te_replacements = [] st.rerun() else: st.markdown( f"""
🖼️ File: {st.session_state.te_pdf_filename} ({st.session_state.te_original_image.width}x{st.session_state.te_original_image.height}px)
""", unsafe_allow_html=True ) # ----------------- OCR BACKGROUND TASK ----------------- if EASYOCR_AVAILABLE and st.session_state.te_ocr_results is None: with st.spinner("Analyzing image text layout using OCR engine..."): ocr_res = TextEditorOrchestrator.run_ocr(st.session_state.te_original_image) st.session_state.te_ocr_results = ocr_res # ----------------- DUAL COLUMN LAYOUT ----------------- col_preview, col_editor_panel = st.columns([1, 1]) # Left Column: Image Previews (Before/After) with col_preview: st.markdown('

1. ORIGINAL SOURCE DOCUMENT

', unsafe_allow_html=True) # Display image with circular number badges if overlay_ocr and st.session_state.te_ocr_results: original_badged = draw_ocr_overlays_with_badges(st.session_state.te_original_image, st.session_state.te_ocr_results) st.image(original_badged, use_container_width=True) else: st.image(st.session_state.te_original_image, use_container_width=True) st.markdown('

2. EDITED DOCUMENT PREVIEW

', unsafe_allow_html=True) if st.session_state.te_edited_image is not None: st.image(st.session_state.te_edited_image, use_container_width=True) else: st.markdown( """

No edits made yet.
Modify a text block on the right panel to see updates.

""", unsafe_allow_html=True ) # Right Column: Visual In-Place Editor Panel with col_editor_panel: st.markdown('

3. CLICK & EDIT DOCUMENT TEXT FIELDS

', unsafe_allow_html=True) if not st.session_state.te_ocr_results: st.warning("⚠️ No text detected in this page. Try uploading an image with higher contrast or clear characters.") else: # Present list of all detected blocks as visual numbered rows st.markdown('
', unsafe_allow_html=True) # Dictionary to store user's changed inputs user_replacements = {} for idx, item in enumerate(st.session_state.te_ocr_results): # Bounding Box description row orig_text = item["text"] bbox = item["bbox"] st.markdown( f"""
{idx+1} Original: "{orig_text}"
""", unsafe_allow_html=True ) # Edit input box rep_input = st.text_input( "Replacement Value", value=orig_text, key=f"rep_val_{idx}", label_visibility="collapsed" ) # Store if the user actually edited it if rep_input != orig_text: user_replacements[idx] = { "bbox": bbox, "original_text": orig_text, "replacement_text": rep_input } st.markdown('
', unsafe_allow_html=True) # Apply Corrections Button col_btn_l, col_btn_c = st.columns([3, 1]) with col_btn_c: apply_batch = st.button("Apply Corrections", use_container_width=True) # ----------------- DYNAMIC GOOGLE FONT MATCHING GALLERY ----------------- st.markdown('

4. MATCHING GOOGLE FONT PREVIEW GALLERY

', unsafe_allow_html=True) st.info("💡 **Google Font Search**: Choose a text block on the left and see how it renders in these popular Google Fonts in real-time, then type the matching name into **Or Search Google Font** in the sidebar!") # Let user pick which replacement string to preview in the gallery words_to_preview = [item["text"] for item in st.session_state.te_ocr_results] selected_preview_word = st.selectbox("Select word to preview fonts:", words_to_preview) # Font Showcase grid st.markdown('
', unsafe_allow_html=True) hindi_showcase_fonts = { "Rozha One": "Thick Display Serifs (Perfect for headers)", "Noto Sans Devanagari": "Sleek Modern Sans-Serif", "Yatra One": "Classic Indian Calligraphy", "Kalam": "Handwritten/Artistic Calligraphy", "Hind": "Clean corporate Sans-Serif", "Martel": "Formal Document Serif", "Teko": "Condensed Bold Display" } # Display 3 popular fonts previews side-by-side col_f1, col_f2 = st.columns(2) font_list = list(hindi_showcase_fonts.keys()) for i, font_name in enumerate(font_list): col_target = col_f1 if i % 2 == 0 else col_f2 with col_target: st.markdown(f"**{font_name}** (*{hindi_showcase_fonts[font_name]}*)") preview_pil = generate_font_preview_pil(selected_preview_word, font_name) st.image(preview_pil, use_container_width=True) st.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # ----------------- RUN CORRECTIONS ----------------- if apply_batch: if not user_replacements: st.warning("⚠️ No changes were made in any text field inputs. Modify a text box first!") else: with st.spinner("Applying all visual text corrections..."): canvas = st.session_state.te_original_image.copy() # Apply each replacement in order for idx in sorted(user_replacements.keys()): item = user_replacements[idx] bbox = item["bbox"] new_text = item["replacement_text"] rep_instruction = { "bbox": bbox, "replacement_text": new_text } canvas = TextEditorOrchestrator.apply_replacements( pil_image=canvas, replacements=[rep_instruction], font_family=font_family, size_multiplier=size_multiplier ) st.session_state.te_replacements.append({ "original": item["original_text"], "replacement": new_text, "bbox": bbox }) st.session_state.te_edited_image = canvas st.session_state.te_pdf_pages[st.session_state.te_active_page] = canvas st.toast("Success! All selected text blocks updated.", icon="✨") st.rerun() # ----------------- DOWNLOAD COMPLETED DOCUMENT ----------------- if st.session_state.te_edited_image is not None or len(st.session_state.te_replacements) > 0: st.markdown( """

💾 EXPORT DOCUMENT

""", unsafe_allow_html=True ) st.markdown('
', unsafe_allow_html=True) filename_base = st.session_state.te_pdf_filename.rsplit('.', 1)[0] if st.session_state.te_file_type == "pdf": col_dl_pdf, col_dl_cur = st.columns(2) with col_dl_pdf: with st.spinner("Compiling all multi-page PDF pages..."): compiled_pdf_bytes = compile_images_to_pdf(st.session_state.te_pdf_pages) st.markdown( """
📄

Complete Edited PDF

Re-compiles all pages into a single PDF document.

""", unsafe_allow_html=True ) st.download_button( label="Download Full Document PDF", data=compiled_pdf_bytes, file_name=f"{filename_base}_edited.pdf", mime="application/pdf", key="dl_edited_pdf", use_container_width=True ) with col_dl_cur: pg_buf = io.BytesIO() st.session_state.te_edited_image.save(pg_buf, format="PNG") pg_bytes = pg_buf.getvalue() st.markdown( """
🖼️

Active Page Image (PNG)

Downloads the current page as a single high-DPI image.

""", unsafe_allow_html=True ) st.download_button( label=f"Download Page {st.session_state.te_active_page+1} PNG", data=pg_bytes, file_name=f"{filename_base}_page_{st.session_state.te_active_page+1}_edited.png", mime="image/png", key="dl_edited_page_png", use_container_width=True ) else: img_buf = io.BytesIO() st.session_state.te_edited_image.save(img_buf, format="PNG") img_bytes = img_buf.getvalue() col_dl_img_l, col_dl_img_c, col_dl_img_r = st.columns([1, 2, 1]) with col_dl_img_c: st.markdown( """

Edited Image Output

Downloads the text-corrected image as high-fidelity PNG.

""", unsafe_allow_html=True ) st.download_button( label="Download Edited Image PNG", data=img_bytes, file_name=f"{filename_base}_edited.png", mime="image/png", key="dl_edited_img_png", use_container_width=True ) st.markdown('
', unsafe_allow_html=True) st.markdown("#### 📜 REPLACEMENT LOGS") log_content = "" for i, rep in enumerate(st.session_state.te_replacements): log_content += f"{i+1}. Erased **'{rep['original']}'** and wrote **'{rep['replacement']}'** at bounding box `{rep['bbox']}`.\n" st.markdown(log_content)