Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,326 +1,142 @@
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
-
import uuid
|
3 |
-
import base64
|
4 |
-
import io
|
5 |
import random
|
6 |
-
import
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
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 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
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 |
-
buffered = io.BytesIO()
|
72 |
-
pil_image.save(buffered, format="PNG")
|
73 |
-
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
74 |
-
|
75 |
-
# --- HTML Templates with Upgraded UI/UX ---
|
76 |
-
|
77 |
-
HOME_PAGE_TEMPLATE = """
|
78 |
-
<!DOCTYPE html>
|
79 |
-
<html lang="en">
|
80 |
-
<head>
|
81 |
-
<meta charset="UTF-8">
|
82 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
83 |
-
<title>Secure Image Scrambler</title>
|
84 |
-
<style>
|
85 |
-
:root { --primary-color: #3b82f6; --hover-color: #2563eb; --bg-color: #f8fafc; --text-color: #334155; --border-color: #cbd5e1; }
|
86 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; background-color: var(--bg-color); color: var(--text-color); margin: 0; padding: 2rem; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
87 |
-
.container { max-width: 650px; width: 100%; margin: auto; background: #fff; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
|
88 |
-
h1 { color: #1e293b; text-align: center; margin-bottom: 0.5rem; }
|
89 |
-
.subtitle { text-align: center; color: #64748b; margin-bottom: 2.5rem; }
|
90 |
-
form > div { margin-bottom: 1.5rem; }
|
91 |
-
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #475569; }
|
92 |
-
input[type="number"] { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 8px; box-sizing: border-box; font-size: 1rem; transition: border-color 0.2s, box-shadow 0.2s; }
|
93 |
-
input[type="number"]:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); outline: none; }
|
94 |
-
button { width: 100%; padding: 0.85rem; background-color: var(--primary-color); color: white; border: none; border-radius: 8px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
|
95 |
-
button:hover { background-color: var(--hover-color); }
|
96 |
-
.error { color: #ef4444; background-color: #fee2e2; border: 1px solid #fecaca; padding: 1rem; border-radius: 8px; margin-top: 1.5rem; text-align: center; }
|
97 |
-
/* Drop Zone Styles */
|
98 |
-
.drop-zone { border: 2px dashed var(--border-color); border-radius: 12px; padding: 40px; text-align: center; cursor: pointer; transition: border-color 0.3s, background-color 0.3s; }
|
99 |
-
.drop-zone--over { border-color: var(--primary-color); background-color: #eff6ff; }
|
100 |
-
.drop-zone__input { display: none; }
|
101 |
-
.drop-zone__prompt { color: #64748b; font-size: 1.1rem; }
|
102 |
-
.drop-zone__prompt strong { color: var(--primary-color); }
|
103 |
-
</style>
|
104 |
-
</head>
|
105 |
-
<body>
|
106 |
-
<div class="container">
|
107 |
-
<h1>🖼️ Secure Image Scrambler</h1>
|
108 |
-
<p class="subtitle">Upload an image to create a scrambled, downloadable version and a protected, non-downloadable preview.</p>
|
109 |
-
<form action="/process" method="post" enctype="multipart/form-data">
|
110 |
-
<div>
|
111 |
-
<label>Upload Image</label>
|
112 |
-
<div class="drop-zone">
|
113 |
-
<span class="drop-zone__prompt">Drag & drop an image here, or <strong>click to select</strong></span>
|
114 |
-
<input type="file" name="image_file" class="drop-zone__input" accept="image/*" required>
|
115 |
-
</div>
|
116 |
-
</div>
|
117 |
-
<div>
|
118 |
-
<label for="grid_size">Grid Size (e.g., 8 for 8x8):</label>
|
119 |
-
<input type="number" id="grid_size" name="grid_size" value="16" min="2" max="64" required>
|
120 |
-
</div>
|
121 |
-
<div>
|
122 |
-
<label for="seed">Scramble Seed (for reproducibility):</label>
|
123 |
-
<input type="number" id="seed" name="seed" value="{{ random_seed }}" required>
|
124 |
-
</div>
|
125 |
-
<button type="submit">Scramble Image</button>
|
126 |
-
</form>
|
127 |
-
{% if error %}
|
128 |
-
<p class="error">{{ error }}</p>
|
129 |
-
{% endif %}
|
130 |
-
</div>
|
131 |
-
|
132 |
-
<script>
|
133 |
-
document.querySelectorAll(".drop-zone__input").forEach(inputElement => {
|
134 |
-
const dropZoneElement = inputElement.closest(".drop-zone");
|
135 |
-
|
136 |
-
dropZoneElement.addEventListener("click", e => {
|
137 |
-
inputElement.click();
|
138 |
-
});
|
139 |
-
|
140 |
-
inputElement.addEventListener("change", e => {
|
141 |
-
if (inputElement.files.length) {
|
142 |
-
updateThumbnail(dropZoneElement, inputElement.files[0]);
|
143 |
-
}
|
144 |
-
});
|
145 |
-
|
146 |
-
dropZoneElement.addEventListener("dragover", e => {
|
147 |
-
e.preventDefault();
|
148 |
-
dropZoneElement.classList.add("drop-zone--over");
|
149 |
-
});
|
150 |
-
|
151 |
-
["dragleave", "dragend"].forEach(type => {
|
152 |
-
dropZoneElement.addEventListener(type, e => {
|
153 |
-
dropZoneElement.classList.remove("drop-zone--over");
|
154 |
-
});
|
155 |
-
});
|
156 |
-
|
157 |
-
dropZoneElement.addEventListener("drop", e => {
|
158 |
-
e.preventDefault();
|
159 |
-
if (e.dataTransfer.files.length) {
|
160 |
-
inputElement.files = e.dataTransfer.files;
|
161 |
-
updateThumbnail(dropZoneElement, e.dataTransfer.files[0]);
|
162 |
-
}
|
163 |
-
dropZoneElement.classList.remove("drop-zone--over");
|
164 |
-
});
|
165 |
-
});
|
166 |
-
|
167 |
-
function updateThumbnail(dropZoneElement, file) {
|
168 |
-
let promptElement = dropZoneElement.querySelector(".drop-zone__prompt");
|
169 |
-
if (promptElement) {
|
170 |
-
promptElement.textContent = `Selected: ${file.name}`;
|
171 |
-
}
|
172 |
-
}
|
173 |
-
</script>
|
174 |
-
</body>
|
175 |
-
</html>
|
176 |
-
"""
|
177 |
-
|
178 |
-
RESULTS_PAGE_TEMPLATE = """
|
179 |
-
<!DOCTYPE html>
|
180 |
-
<html lang="en">
|
181 |
-
<head>
|
182 |
-
<meta charset="UTF-8">
|
183 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
184 |
-
<title>Scrambled Results</title>
|
185 |
-
<style>
|
186 |
-
:root { --primary-color: #3b82f6; --bg-color: #f8fafc; --text-color: #334155; }
|
187 |
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; background-color: var(--bg-color); color: var(--text-color); margin: 0; padding: 2rem; }
|
188 |
-
.container { max-width: 1400px; margin: auto; }
|
189 |
-
.header { text-align: center; margin-bottom: 3rem; }
|
190 |
-
h1 { color: #1e293b; margin-bottom: 0.5rem; }
|
191 |
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; }
|
192 |
-
.card { background: #fff; padding: 1.5rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); display: flex; flex-direction: column; }
|
193 |
-
h2 { border-bottom: 1px solid #e2e8f0; padding-bottom: 0.75rem; margin-top: 0; font-size: 1.25rem; color: #1e293b; }
|
194 |
-
.card p { color: #64748b; flex-grow: 1; }
|
195 |
-
.image-container { margin-bottom: 1.5rem; text-align: center; }
|
196 |
-
img, canvas { max-width: 100%; height: auto; border: 1px solid #e2e8f0; border-radius: 8px; image-rendering: pixelated; }
|
197 |
-
.canvas-container { background: repeating-conic-gradient(#f1f5f9 0% 25%, transparent 0% 50%) 50% / 20px 20px; border-radius: 8px; }
|
198 |
-
.download-button, .home-link { display: block; width: 100%; text-align: center; padding: 0.75rem; background-color: var(--primary-color); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: background-color 0.2s; box-sizing: border-box; }
|
199 |
-
.download-button:hover, .home-link:hover { background-color: #2563eb; }
|
200 |
-
.home-link { background-color: #64748b; margin-top: 3rem; display: inline-block; width: auto; padding: 0.75rem 2rem; }
|
201 |
-
</style>
|
202 |
-
</head>
|
203 |
-
<body>
|
204 |
-
<div class="container">
|
205 |
-
<div class="header">
|
206 |
-
<h1>Results</h1>
|
207 |
-
<p>Your image has been processed. You can download the scrambled assets or view the unscrambled version below.</p>
|
208 |
-
</div>
|
209 |
-
<div class="grid">
|
210 |
-
<div class="card">
|
211 |
-
<h2>Unscrambled Preview (Protected)</h2>
|
212 |
-
<div class="image-container canvas-container">
|
213 |
-
<canvas id="unscrambled-canvas" width="{{ width }}" height="{{ height }}"></canvas>
|
214 |
-
</div>
|
215 |
-
<p>This is rendered on a canvas and is not a downloadable file. Right-clicking is disabled.</p>
|
216 |
-
</div>
|
217 |
-
<div class="card">
|
218 |
-
<h2>Scrambled Image</h2>
|
219 |
-
<div class="image-container">
|
220 |
-
<img src="{{ scrambled_image_url }}" alt="Scrambled Image">
|
221 |
-
</div>
|
222 |
-
<p>This is the shuffled version of your image. It can be unscrambled with the map file.</p>
|
223 |
-
<a href="{{ scrambled_image_url }}" download class="download-button">Download Scrambled PNG</a>
|
224 |
-
</div>
|
225 |
-
<div class="card">
|
226 |
-
<h2>Scrambling Map File</h2>
|
227 |
-
<div class="image-container">
|
228 |
-
<img src="{{ map_image_url }}" alt="Scrambling Map Visualization">
|
229 |
-
</div>
|
230 |
-
<p>This file contains the data needed to reconstruct the original image from the scrambled one.</p>
|
231 |
-
<a href="{{ map_json_url }}" download class="download-button">Download Map JSON</a>
|
232 |
-
</div>
|
233 |
-
</div>
|
234 |
-
<div style="text-align: center;">
|
235 |
-
<a href="/" class="home-link">Scramble Another Image</a>
|
236 |
-
</div>
|
237 |
-
</div>
|
238 |
-
|
239 |
-
<script>
|
240 |
-
const canvas = document.getElementById('unscrambled-canvas');
|
241 |
-
const ctx = canvas.getContext('2d');
|
242 |
-
const img = new Image();
|
243 |
-
canvas.oncontextmenu = (e) => { e.preventDefault(); return false; };
|
244 |
-
img.src = "data:image/png;base64,{{ unscrambled_base64 }}";
|
245 |
-
img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
246 |
-
</script>
|
247 |
-
</body>
|
248 |
-
</html>
|
249 |
-
"""
|
250 |
-
|
251 |
-
# --- Flask Routes ---
|
252 |
-
|
253 |
-
@app.route('/')
|
254 |
-
def home():
|
255 |
-
"""Renders the main upload page."""
|
256 |
-
return render_template_string(HOME_PAGE_TEMPLATE, random_seed=random.randint(10000, 99999))
|
257 |
-
|
258 |
-
@app.route('/process', methods=['POST'])
|
259 |
-
def process_image():
|
260 |
-
"""Handles the image processing."""
|
261 |
-
if 'image_file' not in request.files or request.files['image_file'].filename == '':
|
262 |
-
return render_template_string(HOME_PAGE_TEMPLATE, error="No image file selected. Please upload an image.")
|
263 |
-
|
264 |
-
file = request.files['image_file']
|
265 |
-
try:
|
266 |
-
grid_size = int(request.form.get('grid_size', 16))
|
267 |
-
seed = int(request.form.get('seed', 12345))
|
268 |
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import numpy as np
|
3 |
+
from PIL import Image
|
4 |
+
import json
|
5 |
import os
|
|
|
|
|
|
|
6 |
import random
|
7 |
+
import math
|
8 |
+
|
9 |
+
# Create a directory for temporary files if it doesn't exist
|
10 |
+
if not os.path.exists("temp"):
|
11 |
+
os.makedirs("temp")
|
12 |
+
|
13 |
+
def scramble_image(image_pil, block_size):
|
14 |
+
"""
|
15 |
+
Scrambles an image by dividing it into blocks and shuffling their positions.
|
16 |
+
|
17 |
+
Args:
|
18 |
+
image_pil (PIL.Image.Image): The input image.
|
19 |
+
block_size (int): The size of the square blocks to divide the image into.
|
20 |
+
|
21 |
+
Returns:
|
22 |
+
tuple: A tuple containing:
|
23 |
+
- PIL.Image.Image: The scrambled image.
|
24 |
+
- dict: The JSON data representing the scramble map.
|
25 |
+
- str: The file path to the saved JSON file for download.
|
26 |
+
"""
|
27 |
+
if image_pil is None:
|
28 |
+
raise gr.Error("Please upload an image first!")
|
29 |
+
|
30 |
+
# Convert PIL Image to a NumPy array
|
31 |
+
img_np = np.array(image_pil)
|
32 |
+
h, w, _ = img_np.shape
|
33 |
+
|
34 |
+
# Calculate the number of blocks, ignoring edges that don't fit a full block
|
35 |
+
num_blocks_h = h // block_size
|
36 |
+
num_blocks_w = w // block_size
|
37 |
+
|
38 |
+
if num_blocks_h == 0 or num_blocks_w == 0:
|
39 |
+
raise gr.Error(f"Image is too small for a block size of {block_size}. Please use a smaller block size.")
|
40 |
+
|
41 |
+
# Create a list of block indices
|
42 |
+
block_indices = list(range(num_blocks_h * num_blocks_w))
|
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 & Key Generator
|
103 |
+
Drag and drop an image below, choose a block size, and hit "Scramble" to generate a scrambled version
|
104 |
+
and a downloadable JSON key file that describes how to unscramble it.
|
105 |
+
"""
|
106 |
+
)
|
107 |
+
|
108 |
+
with gr.Row(variant="panel"):
|
109 |
+
# Column 1: Inputs
|
110 |
+
with gr.Column(scale=1):
|
111 |
+
# The gr.Image component automatically creates a file drop area
|
112 |
+
image_input = gr.Image(
|
113 |
+
type="pil",
|
114 |
+
label="Drop Your Image Here",
|
115 |
+
height=300
|
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 |
+
submit_btn = gr.Button("Scramble Image", variant="primary")
|
125 |
+
|
126 |
+
# Column 2: Outputs
|
127 |
+
with gr.Column(scale=2):
|
128 |
+
scrambled_output_image = gr.Image(label="Scrambled Result", height=400, interactive=False)
|
129 |
+
with gr.Accordion("View & Download Scramble Key", open=False):
|
130 |
+
json_output_display = gr.JSON(label="Scramble Key Data")
|
131 |
+
# The gr.File component provides the download button
|
132 |
+
json_output_file = gr.File(label="Download Key File (.json)", file_count="single")
|
133 |
+
|
134 |
+
# Define the click action: inputs -> function -> outputs
|
135 |
+
submit_btn.click(
|
136 |
+
fn=scramble_image,
|
137 |
+
inputs=[image_input, block_size_slider],
|
138 |
+
outputs=[scrambled_output_image, json_output_display, json_output_file]
|
139 |
+
)
|
140 |
+
|
141 |
+
if __name__ == "__main__":
|
142 |
+
demo.launch()
|