broadfield-dev commited on
Commit
2cbb464
·
verified ·
1 Parent(s): 6c81541

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -323
app.py CHANGED
@@ -1,326 +1,142 @@
 
 
 
 
1
  import os
2
- import uuid
3
- import base64
4
- import io
5
  import random
6
- import json
7
- from flask import Flask, request, render_template_string, send_from_directory, url_for
8
- from PIL import Image, ImageDraw, ImageFont
9
- import numpy as np
10
-
11
- # --- Configuration ---
12
- HOST = '0.0.0.0'
13
- PORT = 7860
14
- UPLOAD_FOLDER = 'temp_files'
15
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
16
-
17
- app = Flask(__name__)
18
- app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
19
-
20
- # --- Image Processing Logic (Unchanged) ---
21
- # ... (The image processing functions are the same, so they are collapsed here for brevity)
22
- # You can copy them from the previous Flask answer. For clarity, they are included here again.
23
-
24
- def process_image_for_grid(image_pil, grid_size):
25
- img_width, img_height = image_pil.size
26
- tile_w, tile_h = img_width // grid_size, img_height // grid_size
27
- if tile_w == 0 or tile_h == 0: return None, None, None
28
- cropped_image = image_pil.crop((0, 0, tile_w * grid_size, tile_h * grid_size))
29
- return cropped_image, tile_w, tile_h
30
-
31
- def scramble_image(image_pil, grid_size, seed):
32
- cropped_image, tile_w, tile_h = process_image_for_grid(image_pil, grid_size)
33
- if not cropped_image: return None, None
34
- 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)]
35
- rng = np.random.default_rng(seed=int(seed))
36
- scramble_map = rng.permutation(len(tiles))
37
- scrambled_image = Image.new('RGB', cropped_image.size)
38
- for new_pos_index, original_tile_index in enumerate(scramble_map):
39
- i, j = divmod(new_pos_index, grid_size)
40
- scrambled_image.paste(tiles[original_tile_index], (j * tile_w, i * tile_h))
41
- return scrambled_image, scramble_map
42
-
43
- def unscramble_image(scrambled_pil, scramble_map, grid_size):
44
- cropped_image, tile_w, tile_h = process_image_for_grid(scrambled_pil, grid_size)
45
- if not cropped_image: return None
46
- 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)]
47
- unscrambled_image = Image.new('RGB', cropped_image.size)
48
- for new_pos_index, original_pos_index in enumerate(scramble_map):
49
- i, j = divmod(original_pos_index, grid_size)
50
- unscrambled_image.paste(scrambled_tiles[new_pos_index], (j * tile_w, i * tile_h))
51
- return unscrambled_image
52
-
53
- def create_mapping_visualization(scramble_map, grid_size):
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("DejaVuSans.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
- 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
- input_image = Image.open(file.stream).convert("RGB")
270
-
271
- # 1. Scramble
272
- scrambled_img, scramble_map = scramble_image(input_image, grid_size, seed)
273
- if scrambled_img is None:
274
- return render_template_string(HOME_PAGE_TEMPLATE, error=f"Image is too small for a {grid_size}x{grid_size} grid. Try a larger image or smaller grid.")
275
-
276
- # 2. Unscramble in memory for preview
277
- unscrambled_img = unscramble_image(scrambled_img, scramble_map, grid_size)
278
- unscrambled_base64 = pil_to_base64(unscrambled_img)
279
-
280
- # 3. Create visualization map image
281
- map_viz_img = create_mapping_visualization(scramble_map, grid_size)
282
-
283
- # 4. Prepare and save downloadable files
284
- unique_id = uuid.uuid4()
285
-
286
- # Scrambled PNG
287
- scrambled_filename = f"{unique_id}_scrambled.png"
288
- scrambled_img.save(os.path.join(app.config['UPLOAD_FOLDER'], scrambled_filename))
289
-
290
- # Map Visualization PNG
291
- map_viz_filename = f"{unique_id}_map_viz.png"
292
- map_viz_img.save(os.path.join(app.config['UPLOAD_FOLDER'], map_viz_filename))
293
-
294
- # NEW: Create and save the JSON map file
295
- map_json_filename = f"{unique_id}_map.json"
296
- map_data = {
297
- "gridSize": grid_size,
298
- "seed": seed,
299
- "width": scrambled_img.width,
300
- "height": scrambled_img.height,
301
- "scrambleMap": scramble_map.tolist() # Convert numpy array to list for JSON
302
- }
303
- with open(os.path.join(app.config['UPLOAD_FOLDER'], map_json_filename), 'w') as f:
304
- json.dump(map_data, f, indent=2)
305
-
306
- # 5. Render the results page with links to all files
307
- return render_template_string(
308
- RESULTS_PAGE_TEMPLATE,
309
- scrambled_image_url=url_for('get_file', filename=scrambled_filename),
310
- map_image_url=url_for('get_file', filename=map_viz_filename),
311
- map_json_url=url_for('get_file', filename=map_json_filename), # New URL for JSON
312
- unscrambled_base64=unscrambled_base64,
313
- width=unscrambled_img.width,
314
- height=unscrambled_img.height
315
- )
316
-
317
- except Exception as e:
318
- return render_template_string(HOME_PAGE_TEMPLATE, error=f"An error occurred: {e}")
319
-
320
- @app.route('/temp_files/<filename>')
321
- def get_file(filename):
322
- """Serves the generated files from the temporary directory."""
323
- return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
324
-
325
- if __name__ == '__main__':
326
- app.run(host=HOST, port=PORT)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()