# Import modular components import os os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "false" from agno.models.google import Gemini import base64 import time import cv2 import numpy as np import streamlit as st from components.sidebar import render_sidebar from services.gemini_service import ( analyze_feasibility_with_gemini, refine_svg_with_gemini, ) from services.image_processor import skeletonize_image from services.pdf_processor import process_pdf from services.vectorizer import generate_svg # Import rembg for background isolation try: from rembg import remove REMBG_AVAILABLE = True except ImportError: REMBG_AVAILABLE = False # Attempt to import PyMuPDF try: import fitz # PyMuPDF PDF_SUPPORT_AVAILABLE = True except ImportError: PDF_SUPPORT_AVAILABLE = False from config.session import init_session, reset_generation_state # ------------------------------------------------------------- # SAFE CALLBACK FUNCTIONS (Prevents State Exceptions) # ------------------------------------------------------------- def load_recommended_settings(params): """Stage 1: Safely sets baseline AI parameter recommendations.""" st.session_state.quality_val = params.get("quality_val", "Moderate (Balanced)") st.session_state.sharpen_val = float(params.get("sharpen_val", 0.0)) st.session_state.scale_val = float(params.get("scale_val", 2.0)) st.session_state.canny_low_val = int(params.get("canny_low_val", 10)) st.session_state.canny_high_val = int(params.get("canny_high_val", 70)) st.session_state.skeletonize_val = bool(params.get("skeletonize_val", False)) def load_refined_settings(params): """Stage 2: Safely sets fine-tuned refinement tweaks.""" st.session_state.quality_val = params.get("quality_val", st.session_state.quality_val) st.session_state.sharpen_val = float(params.get("sharpen_val", st.session_state.sharpen_val)) st.session_state.scale_val = float(params.get("scale_val", st.session_state.scale_val)) st.session_state.canny_low_val = int(params.get("canny_low_val", st.session_state.canny_low_val)) st.session_state.canny_high_val = int(params.get("canny_high_val", st.session_state.canny_high_val)) st.session_state.skeletonize_val = bool(params.get("skeletonize_val", st.session_state.skeletonize_val)) st.session_state.contour_thickness_val = int(params.get("contour_thickness_val", st.session_state.contour_thickness_val)) st.session_state.dilate_iter_val = int(params.get("dilate_iter_val", st.session_state.dilate_iter_val)) st.set_page_config(page_title="Handicraft Image Workspace", layout="wide") init_session() # Initialize State management if missing if "pipeline_triggered" not in st.session_state: st.session_state.pipeline_triggered = False if "advisor_insights" not in st.session_state: st.session_state.advisor_insights = None if "advisor_insights_dynamic" not in st.session_state: st.session_state.advisor_insights_dynamic = None # ------------------------------------------------------------- # CALL THE MODULAR SIDEBAR COMPONENT HERE # ------------------------------------------------------------- app_page = render_sidebar(reset_generation_state) # ============================================================= # PAGE 1: ORIGINAL IMAGE-TO-SVG VECTORIZER # ============================================================= if app_page == "๐ŸŽจ Image-to-SVG Vectorizer": st.markdown("# ๐ŸŽจ Advanced Handicraft Image & PDF Vectorizer") st.markdown("Convert photos, sketches, drawings, or multi-page PDF documents into clean, editable vector SVGs with strict AI guidance.") # PDF Processing Mode if PDF_SUPPORT_AVAILABLE: st.subheader("๐Ÿ“„ PDF Processing Mode") pdf_mode = st.radio( "Select how files should be processed:", ["Extract Embedded Images", "Render Entire Pages"], index=0 if st.session_state.pdf_mode_val == "Extract Embedded Images" else 1, help="Extract Embedded Images scans inside the PDF to fetch actual photographic contents. Render Entire Pages snapshots each full page." ) if pdf_mode != st.session_state.pdf_mode_val: st.session_state.pdf_mode_val = pdf_mode st.session_state.current_file_name = "" reset_generation_state() st.session_state.pipeline_triggered = False # File Uploader - Configured to allow multiple uploads for target comparison supported_formats = ["png", "jpg", "jpeg"] if PDF_SUPPORT_AVAILABLE: supported_formats.append("pdf") file_help = "Upload one or more alternative photos of your handicraft (JPG, PNG, or PDF) to let AI evaluate the cleanest candidate." else: file_help = "Upload JPG/PNG images. Note: Install 'pymupdf' (pip install pymupdf) to unlock PDF support." uploaded_files = st.file_uploader(file_help, type=supported_formats, accept_multiple_files=True, key="vectorizer_uploader") if uploaded_files: # Check if the compilation batch changed batch_signature = "".join([f.name for f in uploaded_files]) if batch_signature != st.session_state.get("last_batch_signature", ""): st.session_state.last_batch_signature = batch_signature st.session_state.extracted_images = [] st.session_state.selected_img_index = 0 st.session_state.advisor_insights = None st.session_state.advisor_insights_dynamic = None reset_generation_state() st.session_state.pipeline_triggered = False # Process single or multiple uploaded items into session state assets for f in uploaded_files: file_name = f.name if file_name.lower().endswith(".pdf") and PDF_SUPPORT_AVAILABLE: pdf_data = f.read() st.session_state.extracted_images.extend(process_pdf(pdf_data, st.session_state.pdf_mode_val)) else: file_bytes_raw = f.read() mime_type = "image/png" if f.type == "image/png" else "image/jpeg" st.session_state.extracted_images.append({ "name": file_name, "bytes": file_bytes_raw, "mime_type": mime_type, "analysis": None, "params": None }) total_images = len(st.session_state.extracted_images) # ------------------------------------------------------------- # IMAGE SELECTION & SELECTIVE AI COMPARISON AGENT # ------------------------------------------------------------- if total_images > 1: st.info(f"๐Ÿ“„ **Multiple Variants Detected:** Found **{total_images}** target variants for evaluation.") # Auto-run strict feasibility checks across all items to recommend the best asset with st.spinner("๐Ÿค– Strict AI Assessment Engine evaluating all files for trace compatibility..."): for idx, img in enumerate(st.session_state.extracted_images): if img["analysis"] is None: # Append instructions to enforce strict evaluation rules in the backend framework strict_result = analyze_feasibility_with_gemini(img["bytes"], img["mime_type"]) st.session_state.extracted_images[idx]["analysis"] = strict_result if isinstance(strict_result, dict) and "recommended_parameters" in strict_result: st.session_state.extracted_images[idx]["params"] = strict_result["recommended_parameters"] # Construct comparison board UI st.markdown("### ๐Ÿ“Š AI Cross-Image Comparison Analysis") comparison_data = [] for img in st.session_state.extracted_images: an = img["analysis"] if isinstance(img["analysis"], dict) else {} score = an.get("suitability_score", "N/A") suit = "๐Ÿ‘ High Pass" if an.get("suitable", False) else "โš ๏ธ Low/Noisy (Rejected)" comparison_data.append({ "Asset Name": img["name"], "Traceability Score (Strict)": f"{score}/100", "Status Decision": suit, "Lighting/Contrast Note": an.get("lighting_critique", "N/A")[:90] + "..." }) st.table(comparison_data) selected_page_name = st.selectbox( "Select your preferred item to load into Workspace:", options=[img["name"] for img in st.session_state.extracted_images], index=st.session_state.selected_img_index ) new_idx = [img["name"] for img in st.session_state.extracted_images].index(selected_page_name) if new_idx != st.session_state.selected_img_index: st.session_state.selected_img_index = new_idx st.session_state.advisor_insights = None st.session_state.advisor_insights_dynamic = None reset_generation_state() st.session_state.pipeline_triggered = False st.rerun() else: # Single Image Upload: Automatically run strict analysis instantly without button gate if st.session_state.extracted_images[0]["analysis"] is None: with st.spinner("๐Ÿค– Initializing Strict Visual Feasibility Assessment..."): img = st.session_state.extracted_images[0] strict_result = analyze_feasibility_with_gemini(img["bytes"], img["mime_type"]) st.session_state.extracted_images[0]["analysis"] = strict_result if isinstance(strict_result, dict) and "recommended_parameters" in strict_result: st.session_state.extracted_images[0]["params"] = strict_result["recommended_parameters"] # Finalize context around the Active Target Item active_img = st.session_state.extracted_images[st.session_state.selected_img_index] active_bytes = active_img["bytes"] active_mime = active_img["mime_type"] st.session_state.original_img_b64 = f"data:{active_mime};base64," + base64.b64encode(active_bytes).decode("utf-8") file_bytes_np = np.asarray(bytearray(active_bytes), dtype=np.uint8) img_rgb = cv2.cvtColor(cv2.imdecode(file_bytes_np, cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB) cached_analysis = active_img["analysis"] cached_params = active_img["params"] # ------------------------------------------------------------- # HIGH-VISIBILITY AI HANDICRAFT ALERT / WARNING # ------------------------------------------------------------- if cached_analysis and isinstance(cached_analysis, dict): is_handicraft = cached_analysis.get("is_handicraft", True) if not is_handicraft: st.error( "๐Ÿšจ **Invalid Image Alert: Non-Handicraft Detected!**\n\n" "Our AI Diagnostic Engine analyzed this image and classified it as **NOT a handicraft, drawing, sketch, or craft pattern**.\n\n" "Tracing models and configurations are optimized specifically for artwork outlines. Please upload a valid image for correct results." ) # ------------------------------------------------------------- # THREE-TAB WORKSPACE INTERFACE LAYOUT # ------------------------------------------------------------- tab_analysis, tab_workspace, tab_advisor = st.tabs([ "๐Ÿ“Š AI Diagnostic Analysis", "๐ŸŽจ Control Workspace", "๐Ÿ’ก Interactive AI Advisor & Staging Pro" ]) # --- TAB 1: STRICT AI DIAGNOSTIC ANALYSIS --- with tab_analysis: st.subheader(f"๐Ÿ›ก๏ธ Strict AI Target Quality Verification: `{active_img['name']}`") if cached_analysis and isinstance(cached_analysis, dict): c_score, c_suit = st.columns([1, 2]) with c_score: score = cached_analysis.get("suitability_score", 0) st.metric(label="Traceability Rating (Strict Audit)", value=f"{score}/100") with c_suit: if cached_analysis.get("suitable", True): st.success("##### ๐Ÿ‘ Acceptable Quality\nThe file passes edge-contrast guidelines and is suitable for trace conversion.") else: st.warning("##### โš ๏ธ Strict Review Advisory\nHigh ambient noise, shadows, or clutter detected. Expect some artifacts.") with st.expander("๐Ÿ‘๏ธ View Full Strict Structural Diagnostics Breakdown", expanded=True): col_det1, col_det2 = st.columns(2) with col_det1: st.markdown("**๐Ÿ’ก Illumination & Shading Profile**") st.write(cached_analysis.get("lighting_critique", "N/A")) st.markdown("**๐Ÿ“ Geometric Foreground/Clutter Separation**") st.write(cached_analysis.get("contrast_critique", "N/A")) with col_det2: st.markdown("**๐Ÿ” Edge Resolution & Clarity**") st.write(cached_analysis.get("detail_clarity_critique", "N/A")) st.markdown("**๐Ÿ”ฎ Vectorization Fidelity Forecast**") st.write(cached_analysis.get("line_art_prediction", "N/A")) # Offer one-click baseline ingestion if cached_params: st.write("") st.button( "โš™๏ธ Sync Diagnostic Parameters to Slider Configurations", on_click=load_recommended_settings, args=(cached_params,), use_container_width=True ) else: st.info("No active diagnostic metrics generated. Please make sure the uploaded image is evaluated properly.") # --- TAB 2: ARTWORK FILTERING & CONTROL WORKSPACE --- with tab_workspace: # Setup dynamic processing variables inside OpenCV pipeline memory maps h, w = img_rgb.shape[:2] # Only calculate mathematical visual conversions if explicitly requested by the button matrix if st.session_state.pipeline_triggered: # --- GRABCUT BACKGROUND SEPARATION --- if st.session_state.get("enable_grabcut", False): with st.spinner("Isolating foreground object..."): bbox_padding = st.session_state.get("bbox_padding", 10) gc_mask = np.zeros((h, w), np.uint8) bgdModel = np.zeros((1, 65), np.float64) fgdModel = np.zeros((1, 65), np.float64) rect = (bbox_padding, bbox_padding, w - (2 * bbox_padding), h - (2 * bbox_padding)) cv2.grabCut(img_rgb, gc_mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT) binary_mask = np.where((gc_mask == 2) | (gc_mask == 0), 0, 1).astype("uint8") img_rgb = img_rgb * binary_mask[:, :, np.newaxis] # --- RESIZING & GRAYSCALE CONVERSION --- img_large = cv2.resize(img_rgb, (int(w * st.session_state.scale_val), int(h * st.session_state.scale_val)), interpolation=cv2.INTER_CUBIC) gray = cv2.cvtColor(img_large, cv2.COLOR_RGB2GRAY) # --- SHARPENING / DE-BLUR --- if st.session_state.sharpen_val > 0: blurred_temp = cv2.GaussianBlur(gray, (0, 0), 3.0) gray = cv2.addWeighted(gray, 1.0 + st.session_state.sharpen_val, blurred_temp, -st.session_state.sharpen_val, 0) gray = np.clip(gray, 0, 255).astype(np.uint8) # --- SMOOTHING & CANNY EDGES --- gray = cv2.bilateralFilter(gray, 9, 75, 75) edges = cv2.Canny(gray, st.session_state.canny_low_val, st.session_state.canny_high_val) # --- MORPHOLOGICAL CLOSING (Fills minor gaps in lines) --- kernel_close = np.ones((3, 3), np.uint8) edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel_close, iterations=1) # --- CONTOUR THICKNESS CONSTRAINTS --- if (not st.session_state.skeletonize_val and st.session_state.contour_thickness_val > 1): contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) edges_clean = np.zeros_like(edges) cv2.drawContours(edges_clean, contours, -1, 255, st.session_state.contour_thickness_val) edges = edges_clean if st.session_state.skeletonize_val: edges = skeletonize_image(edges) elif st.session_state.dilate_iter_val > 0: edges = cv2.dilate(edges, np.ones((2, 2), np.uint8), iterations=st.session_state.dilate_iter_val) # --- PREPARE BUFFERS FOR DISPLAY AND VECTORIZATION --- _, edge_buffer = cv2.imencode(".png", edges) st.session_state.line_art_bytes = edge_buffer.tobytes() else: # Default clean array state placeholder before generation trigger edges = np.zeros((h, w), dtype=np.uint8) # --- RENDER COLUMNS FOR SIDE-BY-SIDE MATCHING --- c1, c2, c3 = st.columns(3) with c1: st.subheader("Original Image") st.image(img_rgb, use_container_width=True) with c2: st.subheader("Line Art") if st.session_state.pipeline_triggered: st.image(edges, use_container_width=True, caption="Computed edge trace map state.") else: st.info("Line art rendering process is queued. Click 'Process Image Filters' above to visualize vectorization paths.") # Inline action button for generating Line Art if st.button("๐Ÿš€ Process Line Art", type="primary", use_container_width=False): st.session_state.pipeline_triggered = True st.rerun() with c3: st.subheader("Vector Image") if st.session_state.get("svg_generated", False): b64 = base64.b64encode(st.session_state.svg_bytes).decode() st.markdown(f'', unsafe_allow_html=True) st.download_button( label="๐Ÿ“ฅ Download SVG", data=st.session_state.svg_bytes, file_name=f"primitive_vector_{active_img['name'].replace(' ', '_')}_{int(time.time())}.svg", mime="image/svg+xml", use_container_width=False, ) else: st.info("Awaiting geometric primitive resolution execution mapping steps.") # Inline action button for SVG output generation if st.session_state.pipeline_triggered: if st.button("๐Ÿ† Generate SVG", type="secondary", use_container_width=False): with st.spinner("Executing structural geometric primitive estimation..."): st.session_state.svg_bytes = generate_svg(edges, st.session_state.skeletonize_val) st.session_state.svg_generated = True st.rerun() # Post-Vectorization Refinement Routine Block if st.session_state.get("svg_generated", False): st.markdown("---") st.markdown("### ๐Ÿ” Advanced Post-Vectorization Refinement") st.write("Use Gemini Vision to analyze structural precision gaps between original artwork layout and vector output curves.") if st.button("๐Ÿ” Analyze Generated SVG for Improvements", use_container_width=True): with st.spinner("Executing side-by-side visual analysis..."): refine_result = refine_svg_with_gemini( active_bytes, active_mime, st.session_state.line_art_bytes, ) st.session_state.ai_refinement = refine_result if isinstance(refine_result, dict) and "recommended_parameters" in refine_result: st.session_state.ai_refined_params = refine_result["recommended_parameters"] else: st.session_state.ai_refined_params = None st.rerun() if st.session_state.get("ai_refinement", None): ref = st.session_state.ai_refinement if isinstance(ref, dict) and "error" in ref: st.error(ref["error"]) elif isinstance(ref, dict): if ref.get("is_perfect", False) or st.session_state.ai_refined_params is None: st.balloons() st.success("โœจ **Perfect Vectorization Reached!**\n\nThe vector mapping looks pristine and fully optimized!") else: st.info("๐Ÿ“‹ **AI Vector Alignment Review**") col_ref1, col_ref2 = st.columns(2) with col_ref1: st.markdown("##### ๐Ÿ“ Line Continuity & Fidelity") st.write(ref.get("edge_fidelity_critique", "No structural flaws found.")) with col_ref2: st.markdown("##### ๐Ÿงผ Noise & Background Mud") st.write(ref.get("noise_clutter_critique", "No background clutter found.")) st.markdown("##### ๐Ÿ› ๏ธ Correction Roadmap") st.write(ref.get("the_fix", "Ready to optimize.")) p = st.session_state.ai_refined_params if p: st.write("---") p_cols = st.columns(4) p_cols[0].metric("Target Sharpen", f"{p.get('sharpen_val', 0.0)}") p_cols[1].metric("Target Scale", f"{p.get('scale_val', 2.0)}x") p_cols[2].metric("Target Canny", f"{p.get('canny_low_val', 10)}-{p.get('canny_high_val', 70)}") p_cols[3].metric("Skeletonize", "On" if p.get('skeletonize_val', False) else "Off") st.button( "๐Ÿš€ Apply Refined Settings", type="primary", use_container_width=True, on_click=load_refined_settings, args=(p,), ) # --- TAB 3: INTERACTIVE AI ADVISOR & STAGING PRO --- with tab_advisor: st.subheader("๐Ÿ’ก Interactive Quality Advisor & Staging Consultant") st.write("Is your raw photo struggling to convert into a clean SVG? Let's dynamically diagnose your handicraft image and design a perfect prompt for ChatGPT/DALL-E to generate a clean outline.") st.markdown("---") st.markdown("##### ๐Ÿ” Describe Your Preferences & Image Concerns") # Question 1: Common alternative photo questionnaire has_alt_photos = st.radio( "1. Do you have any alternative photos of this same handicraft?", ["No, this is my only photo", "Yes, I have alternative photos taken under different settings"], index=0, help="Uploading multiple variants allows our AI model to automatically isolate and select the highest contrast asset." ) # Question 2: Free-form outline style description preferred_style = st.text_input( "2. Describe your preferred outline style (e.g., bold and thick stencils, fine-line detailed sketches, simple clean cartoon outlines):", value="Crisp black and white vector outline with solid uniform lines and no inner details", help="Type exactly how you want the generated outlines to look." ) # Question 3: Free-form corrections / opinion input correction_priority = st.text_area( "3. Describe any specific concerns or trace corrections you want the model to resolve:", value="Please completely remove the noisy textured background, connect any faint/broken borders, and ignore all shadows and gray tones.", help="Let us know what parts of the image need adjusting or cleaning." ) st.markdown("---") # Make advisor key unique to the active state inputs to prevent cache collision advisor_key = f"adv_{active_img['name']}_{has_alt_photos[:5]}_{preferred_style[:10]}_{correction_priority[:10]}" if st.session_state.get("current_advisor_key") != advisor_key: st.session_state.advisor_insights = None if st.button("โœจ Generate Custom ChatGPT Prompt & Staging Advice", type="primary", use_container_width=True): st.session_state.current_advisor_key = advisor_key with st.spinner("AI Studio analyzing visual diagnostics and custom preferences..."): from services.gemini_service import prepare_gemini_env, rotator prepare_gemini_env() score = 100 clutter_note = "Minimal clutter detected." lighting_note = "Uniform illumination." if cached_analysis and isinstance(cached_analysis, dict): score = cached_analysis.get("suitability_score", 75) clutter_note = cached_analysis.get("contrast_critique", "N/A") lighting_note = cached_analysis.get("lighting_critique", "N/A") system_instruction = ( "You are an expert handicraft design consultant, product photographer, and vector design specialist. " "You analyze physical image quality constraints and help users generate perfect digital templates using generative AI tools." ) structured_prompt = f""" The user uploaded a handicraft image named '{active_img['name']}'. Image Analysis: - Suitability Score: {score}/100 - Contrast: {clutter_note} - Lighting: {lighting_note} User Preferences: - Outline Style: {preferred_style} - Corrections: {correction_priority} - Alternative Photos: {has_alt_photos} Generate a SHORT and SIMPLE markdown response. Rules: - Maximum 250 words total. - Use short bullet points only. - Do NOT write long paragraphs. - Keep every bullet under 20 words. - Use simple English. - Focus only on useful information. - Avoid repeating the user's preferences. - Be concise. Return exactly these sections: ### โœ… Image Diagnosis โ€ข Overall quality (1 sentence) โ€ข Biggest issue (1 bullet) โ€ข Quick recommendation (1 bullet) ### ๐ŸŽจ ChatGPT / DALLยทE Prompt Provide ONE copy-paste prompt inside a markdown code block. ### ๐Ÿ“ธ Better Photo Tips Give ONLY 3 short bullets. Keep everything easy to scan. """ try: advisor_agent = Gemini( id="gemini-2.5-flash", system_prompt=system_instruction ) from agno.models.message import Message response_object = advisor_agent.response([Message(role="user", content=structured_prompt)]) st.session_state.advisor_insights = response_object.content if hasattr(response_object, 'content') else str(response_object) except Exception as chat_error: rotator.rotate() st.session_state.advisor_insights = f"โš ๏ธ An anomaly occurred while synthesizing recommendations: {str(chat_error)}. Please try again." st.rerun() if st.session_state.get("advisor_insights"): st.markdown("---") st.markdown("### ๐Ÿ”ฎ Your Dynamic AI Advisor Blueprint") st.markdown(st.session_state.advisor_insights) # Render navigation context assistant if alternative images are active if has_alt_photos == "Yes, I have alternative photos taken under different settings": st.info( "๐Ÿ’ก **Quick Navigation:** Drag and drop your alternate photos into the file uploader at the very top. " "The comparison board will automatically run diagnostics and show you which is the cleanest candidate." ) else: st.info("๐Ÿ’ก Fill out your custom preferences above and click the button to generate a personalized ChatGPT prompt and physical staging recipes.") else: st.info("โœจ Please upload one or more alternative photographs, drawings, or PDF documents containing your handicraft designs to begin!") if not PDF_SUPPORT_AVAILABLE: st.warning("โ„น๏ธ **Unlock PDF Support:** Install PyMuPDF (`pip install pymupdf`) to extract pages from PDF files directly in this application.") # ============================================================= # NEW PAGE 2: TOOL EXTRACT (OPENCV GRABCUT & CLIPBOARD) # ============================================================= elif app_page == "๐Ÿ› ๏ธ Tool Extract": st.markdown("# ๐Ÿ› ๏ธ Handicraft Tool Isolator & Extractor") st.markdown("Upload pictures or PDFs of your craft items to isolate tools using standard Computer Vision (GrabCut), convert them into a transparent GIF asset, and copy them directly to your clipboard.") # PDF Processing Configuration for extraction if PDF_SUPPORT_AVAILABLE: st.subheader("๐Ÿ“„ PDF Processing Mode") tool_pdf_mode = st.radio( "Select document extraction method:", ["Extract Embedded Images", "Render Entire Pages"], key="tool_pdf_mode_choice" ) # Unified File Input Formats tool_formats = ["png", "jpg", "jpeg"] if PDF_SUPPORT_AVAILABLE: tool_formats.append("pdf") tool_help = "Upload a JPG, PNG, or multi-page PDF document containing tools." else: tool_help = "Upload JPG/PNG image. Install 'pymupdf' to unpack PDF items." tool_file = st.file_uploader(tool_help, type=tool_formats, key="tool_extractor_uploader") if tool_file: t_images = [] if tool_file.name.lower().endswith(".pdf") and PDF_SUPPORT_AVAILABLE: with st.spinner("Deconstructing pages..."): t_pdf_data = tool_file.read() t_images = process_pdf(t_pdf_data, tool_pdf_mode) else: t_bytes = tool_file.read() t_mime = "image/png" if tool_file.type == "image/png" else "image/jpeg" t_images = [{"name": tool_file.name, "bytes": t_bytes, "mime_type": t_mime}] # Multiple elements selection handler if len(t_images) > 1: selected_tool_img = st.selectbox( "Target design catalog index:", options=[ti["name"] for ti in t_images], key="tool_img_selector" ) active_idx = [ti["name"] for ti in t_images].index(selected_tool_img) else: active_idx = 0 target_tool = t_images[active_idx] # --- GrabCut Custom Parameters --- st.sidebar.markdown("### ๐ŸŽ›๏ธ Isolation Fine-Tuning") bbox_padding = st.sidebar.slider( "Bounding Box Padding", min_value=2, max_value=100, value=15, help="Tells the computer how close the tool is to the edges of the image. Lower values mean the tool spans nearly the whole frame." ) iter_count = st.sidebar.slider("Extraction Accuracy Iterations", min_value=1, max_value=10, value=5) # Draw split presentation boards tc1, tc2 = st.columns(2) with tc1: st.subheader("๐Ÿ“ธ Original Input") st.image(target_tool["bytes"], use_container_width=True) with tc2: st.subheader("โœจ Isolated Tool Output") with st.spinner("Executing CV GrabCut background separation..."): try: from PIL import Image import io # Convert uploaded bytes to OpenCV image format raw_np = np.asarray(bytearray(target_tool["bytes"]), dtype=np.uint8) decoded_img = cv2.imdecode(raw_np, cv2.IMREAD_COLOR) h, w = decoded_img.shape[:2] # Convert to RGB for PIL compatibility later img_rgb = cv2.cvtColor(decoded_img, cv2.COLOR_BGR2RGB) # Initialize mask matrices for GrabCut mask = np.zeros((h, w), np.uint8) bgdModel = np.zeros((1, 65), np.float64) fgdModel = np.zeros((1, 65), np.float64) # Define a rectangle wrapping the foreground tool based on user padding rect = (bbox_padding, bbox_padding, w - (2 * bbox_padding), h - (2 * bbox_padding)) # Execute GrabCut directly using OpenCV cv2.grabCut(img_rgb, mask, rect, bgdModel, fgdModel, iter_count, cv2.GC_INIT_WITH_RECT) # Generate a clean binary mask where background = 0, foreground = 1 # (cv2.GC_PR_FGD and cv2.GC_FGD represent probable and definite foreground) binary_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8") # Apply the mask to isolate the image content isolated_rgb = img_rgb * binary_mask[:, :, np.newaxis] # Create an alpha channel (transparency map) based on the mask alpha_channel = (binary_mask * 255).astype("uint8") # Stack them together to make a clean 4-channel transparent RGBA image r, g, b = cv2.split(isolated_rgb) rgba_mat = cv2.merge([r, g, b, alpha_channel]) # Convert directly to a transparent PIL GIF asset pil_img = Image.fromarray(rgba_mat) gif_io = io.BytesIO() pil_img.save(gif_io, format="GIF", save_all=True, transparency=0, disposal=2) final_bytes = gif_io.getvalue() # Encode asset to Base64 for the browser clipboard handling injection b64_output = base64.b64encode(final_bytes).decode("utf-8") st.image(final_bytes, use_container_width=True, caption="Isolated tool asset ready") except Exception as ex: st.error(f"Failed processing background isolation details: {ex}") final_bytes = None if final_bytes: st.markdown("---") st.markdown("### ๐Ÿ“‹ Export & Distribution") # JavaScript injection to bind image blob string straight inside clipboard framework clipboard_html = f""" """ st.components.v1.html(clipboard_html, height=60) # Standard physical download safety fallback st.download_button( label="๐Ÿ“ฅ Save Transparent GIF Asset Instead", data=final_bytes, file_name=f"isolated_{target_tool['name'].rsplit('.', 1)[0]}.gif", mime="image/gif", use_container_width=True, key="tool_download_fallback" ) elif app_page == "โšก Interactive Node Simplifier": st.markdown("# โšก Interactive Node Simplifier") if st.session_state.svg_bytes: svg_content = st.session_state.svg_bytes.decode('utf-8') vector_editor_html = """
3.0
Original Vector Nodes: -
Current Nodes: -
Nodes Eliminated: -
""".replace("{{escaped_svg_content}}", svg_content.replace("\\", "\\\\").replace("`", "\\`")) st.components.v1.html(vector_editor_html, height=750, scrolling=False) else: st.warning("Please process an image on the first page first.")