import gradio as gr
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import base64
import io
import random
import os
import uuid
import json # <-- Import the json library
# --- Configuration & Setup ---
# Create a directory to store temporary output files
TEMP_DIR = "temp_outputs"
os.makedirs(TEMP_DIR, exist_ok=True)
# --- Core Image Processing Logic (Unchanged) ---
# ... (All the functions like scramble_image, unscramble_image, etc., are the same)
def process_image_for_grid(image_pil, grid_size):
if image_pil is None: return None, 0, 0
img_width, img_height = image_pil.size
tile_w, tile_h = img_width // grid_size, img_height // grid_size
if tile_w == 0 or tile_h == 0:
raise gr.Error(f"Image is too small for a {grid_size}x{grid_size} grid.")
cropped_image = image_pil.crop((0, 0, tile_w * grid_size, tile_h * grid_size))
return cropped_image, tile_w, tile_h
def scramble_image(image_pil, grid_size, seed):
cropped_image, tile_w, tile_h = process_image_for_grid(image_pil, grid_size)
tiles = [cropped_image.crop((j * tile_w, i * tile_h, (j + 1) * tile_w, (i + 1) * tile_h)) for i in range(grid_size) for j in range(grid_size)]
rng = np.random.default_rng(seed=int(seed))
scramble_map = rng.permutation(len(tiles))
scrambled_image = Image.new('RGB', cropped_image.size)
for new_pos_index, original_tile_index in enumerate(scramble_map):
i, j = divmod(new_pos_index, grid_size)
scrambled_image.paste(tiles[original_tile_index], (j * tile_w, i * tile_h))
return scrambled_image, scramble_map
def unscramble_image(scrambled_pil, scramble_map, grid_size):
cropped_image, tile_w, tile_h = process_image_for_grid(scrambled_pil, grid_size)
scrambled_tiles = [cropped_image.crop((j * tile_w, i * tile_h, (j + 1) * tile_w, (i + 1) * tile_h)) for i in range(grid_size) for j in range(grid_size)]
unscrambled_image = Image.new('RGB', cropped_image.size)
for new_pos_index, original_pos_index in enumerate(scramble_map):
i, j = divmod(original_pos_index, grid_size)
unscrambled_image.paste(scrambled_tiles[new_pos_index], (j * tile_w, i * tile_h))
return unscrambled_image
def create_mapping_visualization(scramble_map, grid_size):
map_size=(512, 512)
vis_image = Image.new('RGB', map_size, color='lightgray')
draw = ImageDraw.Draw(vis_image)
try: font = ImageFont.truetype("arial.ttf", size=max(10, 32 - grid_size * 2))
except IOError: font = ImageFont.load_default()
tile_w, tile_h = map_size[0] // grid_size, map_size[1] // grid_size
for new_pos_index, original_pos_index in enumerate(scramble_map):
i, j = divmod(new_pos_index, grid_size)
x0, y0 = j * tile_w, i * tile_h
draw.rectangle([x0, y0, x0 + tile_w, y0 + tile_h], outline='black')
text = str(original_pos_index)
text_bbox = draw.textbbox((0, 0), text, font=font)
text_w, text_h = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
draw.text((x0 + (tile_w - text_w) / 2, y0 + (tile_h - text_h) / 2), text, fill='black', font=font)
return vis_image
def pil_to_base64(pil_image):
buffered = io.BytesIO()
pil_image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode("utf-8")
def create_canvas_html(base64_string, width, height):
return f"""
"""
# --- Main Gradio Function ---
def process_and_display(input_image, grid_size, seed):
"""
Main orchestrator function. Saves PNGs and JSON and returns all file paths.
"""
if input_image is None:
# Return empty placeholders for all outputs
return None, "Please upload an image to begin.
", None, None, None
# 1. Scramble the image
scrambled_img, scramble_map = scramble_image(input_image, grid_size, seed)
# 2. Unscramble for canvas preview
unscrambled_img = unscramble_image(scrambled_img, scramble_map, grid_size)
base64_unscrambled = pil_to_base64(unscrambled_img)
canvas_html = create_canvas_html(base64_unscrambled, unscrambled_img.width, unscrambled_img.height)
# 3. Create map visualization
map_viz_img = create_mapping_visualization(scramble_map, grid_size)
# --- Save all files and get their paths ---
unique_id = uuid.uuid4()
# Save the scrambled image PNG
scrambled_filepath = os.path.join(TEMP_DIR, f"{unique_id}_scrambled.png")
scrambled_img.save(scrambled_filepath)
# Save the map visualization PNG
map_viz_filepath = os.path.join(TEMP_DIR, f"{unique_id}_map.png")
map_viz_img.save(map_viz_filepath)
# NEW: Create and save the JSON map file
map_json_filepath = os.path.join(TEMP_DIR, f"{unique_id}_map.json")
map_data = {
"gridSize": grid_size,
"seed": seed,
"width": scrambled_img.width,
"height": scrambled_img.height,
"scrambleMap": scramble_map.tolist() # Convert numpy array for JSON
}
with open(map_json_filepath, 'w') as f:
json.dump(map_data, f, indent=2)
# 4. Return all necessary file paths and the canvas HTML
return scrambled_filepath, canvas_html, map_viz_filepath, scrambled_filepath, map_json_filepath
# --- Gradio UI Definition ---
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown(
"""
# 🖼️ Secure Image Scrambler & Viewer (v3)
Upload an image to create a scrambled `.png` file and a `.json` map file.
The unscrambled original can be viewed in a protected preview window that prevents easy downloading.
"""
)
with gr.Row():
with gr.Column(scale=1, min_width=350):
input_image = gr.Image(
type="pil",
label="Upload Image or Paste from Clipboard/URL",
sources=["upload", "clipboard"]
)
with gr.Accordion("Settings", open=True):
grid_size_slider = gr.Slider(minimum=2, maximum=32, value=8, step=1, label="Grid Size (NxN)")
seed_input = gr.Number(value=lambda: random.randint(0, 99999), label="Scramble Seed")
submit_btn = gr.Button("Scramble & Process", variant="primary")
with gr.Column(scale=2):
with gr.Tabs():
with gr.TabItem("Downloads"):
gr.Markdown("### Scrambled Image")
scrambled_output = gr.Image(
label="Scrambled Image Preview",
type="filepath",
interactive=False,
)
downloadable_png = gr.File(
label="Download Scrambled PNG File",
interactive=False
)
gr.Markdown("---")
gr.Markdown("### Scrambling Map")
downloadable_json = gr.File(
label="Download Map JSON File",
interactive=False
)
with gr.TabItem("Unscrambled Preview (Protected)"):
unscrambled_canvas = gr.HTML(
label="Unscrambled Preview (Not directly downloadable)"
)
with gr.TabItem("Map Visualization"):
mapping_output = gr.Image(
label="Mapping Key Visualization",
type="filepath",
interactive=False
)
# Connect the button to the main function, mapping all outputs correctly
submit_btn.click(
fn=process_and_display,
inputs=[input_image, grid_size_slider, seed_input],
outputs=[
scrambled_output, # Receives scrambled_filepath for display
unscrambled_canvas, # Receives canvas_html
mapping_output, # Receives map_viz_filepath
downloadable_png, # Receives scrambled_filepath for download
downloadable_json # Receives map_json_filepath for download
]
)
demo.launch()