# 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 = """