Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,142 +1,197 @@
|
|
1 |
import gradio as gr
|
2 |
import numpy as np
|
3 |
-
from PIL import Image
|
4 |
-
import
|
5 |
-
import
|
6 |
import random
|
7 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
-
#
|
10 |
-
if not os.path.exists("temp"):
|
11 |
-
os.makedirs("temp")
|
12 |
|
13 |
-
def
|
14 |
"""
|
15 |
-
|
|
|
|
|
|
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
# Shuffle the indices to create the scramble map
|
44 |
-
scrambled_indices = random.sample(block_indices, len(block_indices))
|
45 |
-
|
46 |
-
# Create a new blank image array to build the scrambled image
|
47 |
-
scrambled_img_np = np.zeros_like(img_np)
|
48 |
-
|
49 |
-
# Reassemble the image using the scrambled map
|
50 |
-
for i, original_idx in enumerate(scrambled_indices):
|
51 |
-
# Position in the new (scrambled) grid
|
52 |
-
new_row, new_col = divmod(i, num_blocks_w)
|
53 |
-
# Position in the original grid
|
54 |
-
original_row, original_col = divmod(original_idx, num_blocks_w)
|
55 |
-
|
56 |
-
# Get the pixel block from the original image
|
57 |
-
block = img_np[
|
58 |
-
original_row * block_size : (original_row + 1) * block_size,
|
59 |
-
original_col * block_size : (original_col + 1) * block_size
|
60 |
-
]
|
61 |
-
|
62 |
-
# Place the block into the new scrambled image
|
63 |
-
scrambled_img_np[
|
64 |
-
new_row * block_size : (new_row + 1) * block_size,
|
65 |
-
new_col * block_size : (new_col + 1) * block_size
|
66 |
-
] = block
|
67 |
-
|
68 |
-
# Convert the scrambled NumPy array back to a PIL Image
|
69 |
-
scrambled_image_pil = Image.fromarray(scrambled_img_np)
|
70 |
-
|
71 |
-
# --- JSON and File Output ---
|
72 |
-
json_data = {
|
73 |
-
"original_dimensions": {"width": w, "height": h},
|
74 |
-
"block_size": block_size,
|
75 |
-
"scramble_map": scrambled_indices
|
76 |
-
}
|
77 |
-
|
78 |
-
# Save the JSON data to a temporary file
|
79 |
-
json_filepath = os.path.join("temp", "scramble_key.json")
|
80 |
-
with open(json_filepath, 'w') as f:
|
81 |
-
json.dump(json_data, f, indent=4)
|
82 |
-
|
83 |
-
return scrambled_image_pil, json_data, json_filepath
|
84 |
-
|
85 |
-
# --- Gradio UI with Styling ---
|
86 |
-
# Use a pre-built theme for a modern look
|
87 |
-
theme = gr.themes.Soft(
|
88 |
-
primary_hue="blue",
|
89 |
-
secondary_hue="sky"
|
90 |
-
).set(
|
91 |
-
# Custom CSS for the file drop zone
|
92 |
-
block_label_background_fill="*primary_50",
|
93 |
-
block_label_border_width="1px",
|
94 |
-
block_label_border_color="*primary_200",
|
95 |
-
block_label_text_color="*primary_500",
|
96 |
-
)
|
97 |
-
|
98 |
-
# Use gr.Blocks for custom layouts
|
99 |
-
with gr.Blocks(theme=theme, css=".gradio-container {max-width: 960px !important}") as demo:
|
100 |
gr.Markdown(
|
101 |
"""
|
102 |
-
# 🖼️ Image Scrambler &
|
103 |
-
|
104 |
-
|
105 |
"""
|
106 |
)
|
107 |
|
108 |
-
with gr.Row(
|
109 |
-
|
110 |
-
|
111 |
-
# The gr.Image component automatically creates a file drop area
|
112 |
-
image_input = gr.Image(
|
113 |
type="pil",
|
114 |
-
label="
|
115 |
-
|
116 |
-
)
|
117 |
-
block_size_slider = gr.Slider(
|
118 |
-
minimum=8,
|
119 |
-
maximum=128,
|
120 |
-
step=8,
|
121 |
-
value=32,
|
122 |
-
label="Scramble Block Size (pixels)"
|
123 |
)
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
# Column 2: Outputs
|
127 |
with gr.Column(scale=2):
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
submit_btn.click(
|
136 |
-
fn=
|
137 |
-
inputs=[
|
138 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
139 |
)
|
140 |
|
141 |
-
|
142 |
-
demo.launch()
|
|
|
1 |
import gradio as gr
|
2 |
import numpy as np
|
3 |
+
from PIL import Image, ImageDraw, ImageFont
|
4 |
+
import base64
|
5 |
+
import io
|
6 |
import random
|
7 |
+
import os
|
8 |
+
import uuid
|
9 |
+
|
10 |
+
# --- Configuration & Setup ---
|
11 |
+
# Create a directory to store temporary output files
|
12 |
+
TEMP_DIR = "temp_outputs"
|
13 |
+
os.makedirs(TEMP_DIR, exist_ok=True)
|
14 |
+
|
15 |
+
|
16 |
+
# --- Core Image Processing Logic (Unchanged) ---
|
17 |
+
# ... (All the functions like scramble_image, unscramble_image, etc., are the same)
|
18 |
+
|
19 |
+
def process_image_for_grid(image_pil, grid_size):
|
20 |
+
"""Crops the image to be perfectly divisible by the grid size."""
|
21 |
+
if image_pil is None: return None, 0, 0
|
22 |
+
img_width, img_height = image_pil.size
|
23 |
+
tile_w = img_width // grid_size
|
24 |
+
tile_h = img_height // grid_size
|
25 |
+
if tile_w == 0 or tile_h == 0:
|
26 |
+
raise gr.Error(f"Image is too small for a {grid_size}x{grid_size} grid. Please use a larger image or a smaller grid size.")
|
27 |
+
cropped_image = image_pil.crop((0, 0, tile_w * grid_size, tile_h * grid_size))
|
28 |
+
return cropped_image, tile_w, tile_h
|
29 |
+
|
30 |
+
def scramble_image(image_pil, grid_size, seed):
|
31 |
+
"""Scrambles an image and returns the PIL image and the map."""
|
32 |
+
cropped_image, tile_w, tile_h = process_image_for_grid(image_pil, grid_size)
|
33 |
+
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)]
|
34 |
+
rng = np.random.default_rng(seed=int(seed))
|
35 |
+
scramble_map = rng.permutation(len(tiles))
|
36 |
+
scrambled_image = Image.new('RGB', cropped_image.size)
|
37 |
+
for new_pos_index, original_tile_index in enumerate(scramble_map):
|
38 |
+
i, j = divmod(new_pos_index, grid_size)
|
39 |
+
scrambled_image.paste(tiles[original_tile_index], (j * tile_w, i * tile_h))
|
40 |
+
return scrambled_image, scramble_map
|
41 |
+
|
42 |
+
def unscramble_image(scrambled_pil, scramble_map, grid_size):
|
43 |
+
"""Unscrambles an image using the provided scramble_map."""
|
44 |
+
cropped_image, tile_w, tile_h = process_image_for_grid(scrambled_pil, grid_size)
|
45 |
+
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)]
|
46 |
+
unscrambled_image = Image.new('RGB', cropped_image.size)
|
47 |
+
for new_pos_index, original_pos_index in enumerate(scramble_map):
|
48 |
+
i, j = divmod(original_pos_index, grid_size)
|
49 |
+
unscrambled_image.paste(scrambled_tiles[new_pos_index], (j * tile_w, i * tile_h))
|
50 |
+
return unscrambled_image
|
51 |
+
|
52 |
+
def create_mapping_visualization(scramble_map, grid_size):
|
53 |
+
"""Creates a PIL image that visualizes the scrambling map."""
|
54 |
+
map_size=(512, 512)
|
55 |
+
vis_image = Image.new('RGB', map_size, color='lightgray')
|
56 |
+
draw = ImageDraw.Draw(vis_image)
|
57 |
+
try: font = ImageFont.truetype("arial.ttf", size=max(10, 32 - grid_size * 2))
|
58 |
+
except IOError: font = ImageFont.load_default()
|
59 |
+
tile_w, tile_h = map_size[0] // grid_size, map_size[1] // grid_size
|
60 |
+
for new_pos_index, original_pos_index in enumerate(scramble_map):
|
61 |
+
i, j = divmod(new_pos_index, grid_size)
|
62 |
+
x0, y0 = j * tile_w, i * tile_h
|
63 |
+
draw.rectangle([x0, y0, x0 + tile_w, y0 + tile_h], outline='black')
|
64 |
+
text = str(original_pos_index)
|
65 |
+
text_bbox = draw.textbbox((0, 0), text, font=font)
|
66 |
+
text_w, text_h = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
|
67 |
+
draw.text((x0 + (tile_w - text_w) / 2, y0 + (tile_h - text_h) / 2), text, fill='black', font=font)
|
68 |
+
return vis_image
|
69 |
+
|
70 |
+
def pil_to_base64(pil_image):
|
71 |
+
"""Converts a PIL Image to a Base64 string for HTML embedding."""
|
72 |
+
buffered = io.BytesIO()
|
73 |
+
pil_image.save(buffered, format="PNG")
|
74 |
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
75 |
+
|
76 |
+
def create_canvas_html(base64_string, width, height):
|
77 |
+
"""Creates the HTML and JavaScript to render an image on a canvas."""
|
78 |
+
return f"""
|
79 |
+
<div style="display: flex; justify-content: center; align-items: center; background: #f0f0f0;">
|
80 |
+
<canvas id="unscrambled-canvas" width="{width}" height="{height}" style="max-width: 100%; max-height: 512px; object-fit: contain;"></canvas>
|
81 |
+
</div>
|
82 |
+
<script>
|
83 |
+
function drawImageOnCanvas() {{
|
84 |
+
const canvas = document.getElementById('unscrambled-canvas');
|
85 |
+
if (!canvas) return;
|
86 |
+
const ctx = canvas.getContext('2d');
|
87 |
+
const img = new Image();
|
88 |
+
img.src = "{base64_string}";
|
89 |
+
img.onload = () => {{ ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }};
|
90 |
+
canvas.oncontextmenu = (e) => {{ e.preventDefault(); return false; }};
|
91 |
+
}}
|
92 |
+
setTimeout(drawImageOnCanvas, 100);
|
93 |
+
</script>
|
94 |
+
"""
|
95 |
|
96 |
+
# --- Main Gradio Function ---
|
|
|
|
|
97 |
|
98 |
+
def process_and_display(input_image, grid_size, seed):
|
99 |
"""
|
100 |
+
Main orchestrator function. Now saves files and returns paths.
|
101 |
+
"""
|
102 |
+
if input_image is None:
|
103 |
+
return None, "<div>Please upload an image to begin.</div>", None, None
|
104 |
|
105 |
+
# 1. Scramble the image
|
106 |
+
scrambled_img, scramble_map = scramble_image(input_image, grid_size, seed)
|
107 |
+
|
108 |
+
# 2. Unscramble for canvas preview
|
109 |
+
unscrambled_img = unscramble_image(scrambled_img, scramble_map, grid_size)
|
110 |
+
base64_unscrambled = pil_to_base64(unscrambled_img)
|
111 |
+
canvas_html = create_canvas_html(base64_unscrambled, unscrambled_img.width, unscrambled_img.height)
|
112 |
|
113 |
+
# 3. Create map visualization
|
114 |
+
map_viz_img = create_mapping_visualization(scramble_map, grid_size)
|
115 |
+
|
116 |
+
# --- NEW: Save files and return paths ---
|
117 |
+
unique_id = uuid.uuid4()
|
118 |
+
|
119 |
+
# Save the scrambled image as a PNG file
|
120 |
+
scrambled_filepath = os.path.join(TEMP_DIR, f"{unique_id}_scrambled.png")
|
121 |
+
scrambled_img.save(scrambled_filepath)
|
122 |
+
|
123 |
+
# Save the map visualization as a PNG file
|
124 |
+
map_viz_filepath = os.path.join(TEMP_DIR, f"{unique_id}_map.png")
|
125 |
+
map_viz_img.save(map_viz_filepath)
|
126 |
+
|
127 |
+
# 4. Return the file paths and HTML to the Gradio components
|
128 |
+
# The file path is returned twice: once for the gr.Image display and once for the gr.File download.
|
129 |
+
return scrambled_filepath, canvas_html, map_viz_filepath, scrambled_filepath
|
130 |
+
|
131 |
+
|
132 |
+
# --- Gradio UI Definition ---
|
133 |
+
|
134 |
+
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
gr.Markdown(
|
136 |
"""
|
137 |
+
# 🖼️ Secure Image Scrambler & Viewer (v2)
|
138 |
+
This tool scrambles an image so you can share it securely. The scrambled version is provided as a downloadable `.png` file.
|
139 |
+
You can view the unscrambled original in a special preview window that prevents easy downloading.
|
140 |
"""
|
141 |
)
|
142 |
|
143 |
+
with gr.Row():
|
144 |
+
with gr.Column(scale=1, min_width=350):
|
145 |
+
input_image = gr.Image(
|
|
|
|
|
146 |
type="pil",
|
147 |
+
label="Upload Image or Paste from Clipboard/URL",
|
148 |
+
sources=["upload", "clipboard"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
)
|
150 |
+
|
151 |
+
with gr.Accordion("Settings", open=True):
|
152 |
+
grid_size_slider = gr.Slider(
|
153 |
+
minimum=2, maximum=32, value=8, step=1, label="Grid Size (NxN)"
|
154 |
+
)
|
155 |
+
seed_input = gr.Number(
|
156 |
+
value=lambda: random.randint(0, 99999), label="Scramble Seed"
|
157 |
+
)
|
158 |
+
|
159 |
+
submit_btn = gr.Button("Scramble & Process", variant="primary")
|
160 |
|
|
|
161 |
with gr.Column(scale=2):
|
162 |
+
with gr.Tabs():
|
163 |
+
with gr.TabItem("Scrambled Image"):
|
164 |
+
scrambled_output = gr.Image(
|
165 |
+
label="Scrambled Image Preview",
|
166 |
+
type="filepath", # This component expects a file path
|
167 |
+
interactive=False,
|
168 |
+
)
|
169 |
+
# NEW: File download component
|
170 |
+
downloadable_file = gr.File(
|
171 |
+
label="Download Scrambled PNG",
|
172 |
+
interactive=False
|
173 |
+
)
|
174 |
+
with gr.TabItem("Unscrambled Preview (Protected)"):
|
175 |
+
unscrambled_canvas = gr.HTML(
|
176 |
+
label="Unscrambled Preview (Not directly downloadable)"
|
177 |
+
)
|
178 |
+
with gr.TabItem("Scrambling Map"):
|
179 |
+
mapping_output = gr.Image(
|
180 |
+
label="Mapping Key Visualization",
|
181 |
+
type="filepath", # This component also expects a file path
|
182 |
+
interactive=False
|
183 |
+
)
|
184 |
+
|
185 |
+
# Connect the button to the main function, mapping outputs correctly
|
186 |
submit_btn.click(
|
187 |
+
fn=process_and_display,
|
188 |
+
inputs=[input_image, grid_size_slider, seed_input],
|
189 |
+
outputs=[
|
190 |
+
scrambled_output, # Receives scrambled_filepath
|
191 |
+
unscrambled_canvas, # Receives canvas_html
|
192 |
+
mapping_output, # Receives map_viz_filepath
|
193 |
+
downloadable_file # Receives scrambled_filepath
|
194 |
+
]
|
195 |
)
|
196 |
|
197 |
+
demo.launch()
|
|