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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -109
app.py CHANGED
@@ -3,22 +3,23 @@ import uuid
3
  import base64
4
  import io
5
  import random
 
6
  from flask import Flask, request, render_template_string, send_from_directory, url_for
7
  from PIL import Image, ImageDraw, ImageFont
8
  import numpy as np
9
 
10
  # --- Configuration ---
11
- # Use os.getenv("PORT", 5000) for compatibility with services like Heroku/Hugging Face
12
- # The host must be '0.0.0.0' to be accessible within the Docker container
13
  HOST = '0.0.0.0'
14
- PORT = 7860 # Hugging Face Spaces exposes port 7860
15
- UPLOAD_FOLDER = 'temp_images'
16
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
17
 
18
  app = Flask(__name__)
19
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
20
 
21
- # --- Image Processing Logic (Copied from previous solution) ---
 
 
22
 
23
  def process_image_for_grid(image_pil, grid_size):
24
  img_width, img_height = image_pil.size
@@ -71,9 +72,8 @@ def pil_to_base64(pil_image):
71
  pil_image.save(buffered, format="PNG")
72
  return base64.b64encode(buffered.getvalue()).decode("utf-8")
73
 
74
- # --- HTML Templates ---
75
 
76
- # Using render_template_string to keep everything in one file
77
  HOME_PAGE_TEMPLATE = """
78
  <!DOCTYPE html>
79
  <html lang="en">
@@ -82,40 +82,95 @@ HOME_PAGE_TEMPLATE = """
82
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
  <title>Secure Image Scrambler</title>
84
  <style>
85
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; background-color: #f8f9fa; color: #343a40; margin: 0; padding: 2rem; }
86
- .container { max-width: 600px; margin: auto; background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
87
- h1 { color: #0056b3; }
88
- form div { margin-bottom: 1.5rem; }
89
- label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
90
- input[type="file"], input[type="number"] { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
91
- button { width: 100%; padding: 0.75rem; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; transition: background-color 0.2s; }
92
- button:hover { background-color: #0056b3; }
93
- .error { color: #dc3545; font-weight: bold; margin-top: 1rem; }
 
 
 
 
 
 
 
 
 
94
  </style>
95
  </head>
96
  <body>
97
  <div class="container">
98
  <h1>🖼️ Secure Image Scrambler</h1>
99
- <p>Upload an image to scramble it. Only the scrambled version will be downloadable.</p>
100
  <form action="/process" method="post" enctype="multipart/form-data">
101
  <div>
102
- <label for="image_file">Choose an image file:</label>
103
- <input type="file" id="image_file" name="image_file" accept="image/*" required>
 
 
 
104
  </div>
105
  <div>
106
  <label for="grid_size">Grid Size (e.g., 8 for 8x8):</label>
107
- <input type="number" id="grid_size" name="grid_size" value="8" min="2" max="64" required>
108
  </div>
109
  <div>
110
- <label for="seed">Scramble Seed (a number for reproducibility):</label>
111
  <input type="number" id="seed" name="seed" value="{{ random_seed }}" required>
112
  </div>
113
  <button type="submit">Scramble Image</button>
114
  </form>
115
  {% if error %}
116
- <p class="error">Error: {{ error }}</p>
117
  {% endif %}
118
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </body>
120
  </html>
121
  """
@@ -128,70 +183,66 @@ RESULTS_PAGE_TEMPLATE = """
128
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
129
  <title>Scrambled Results</title>
130
  <style>
131
- body { font-family: sans-serif; line-height: 1.6; background-color: #f8f9fa; color: #343a40; margin: 0; padding: 2rem; }
132
- .container { max-width: 1200px; margin: auto; }
133
- h1 { color: #0056b3; }
 
 
134
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; }
135
- .card { background: #fff; padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
136
- h2 { border-bottom: 2px solid #eee; padding-bottom: 0.5rem; margin-top: 0; }
137
- p { margin-top: 0; }
138
- img, canvas { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }
139
- .canvas-container { border: 1px solid #ddd; background: #f0f0f0; display: inline-block; line-height: 0; }
140
- a.home-link { display: inline-block; margin-top: 2rem; padding: 0.75rem 1.5rem; background-color: #6c757d; color: white; text-decoration: none; border-radius: 4px; transition: background-color 0.2s; }
141
- a.home-link:hover { background-color: #5a6268; }
 
 
142
  </style>
143
  </head>
144
  <body>
145
  <div class="container">
146
- <h1>Results</h1>
 
 
 
147
  <div class="grid">
148
  <div class="card">
149
  <h2>Unscrambled Preview (Protected)</h2>
150
- <p>This is rendered on a canvas and is not a downloadable file.</p>
151
- <div class="canvas-container">
152
  <canvas id="unscrambled-canvas" width="{{ width }}" height="{{ height }}"></canvas>
153
  </div>
 
154
  </div>
155
  <div class="card">
156
- <h2>Scrambled Image (Downloadable)</h2>
157
- <p>Right-click and "Save Image As..." to download this scrambled version.</p>
158
- <a href="{{ scrambled_image_url }}" download>
159
  <img src="{{ scrambled_image_url }}" alt="Scrambled Image">
160
- </a>
 
 
161
  </div>
162
  <div class="card">
163
- <h2>Scrambling Map</h2>
164
- <p>This map shows the original position of the tile in each new spot.</p>
165
- <a href="{{ map_image_url }}" download>
166
- <img src="{{ map_image_url }}" alt="Scrambling Map">
167
- </a>
 
168
  </div>
169
  </div>
170
- <a href="/" class="home-link">Scramble Another Image</a>
 
 
171
  </div>
172
 
173
  <script>
174
- // This script is guaranteed to run after the canvas element exists in the DOM.
175
  const canvas = document.getElementById('unscrambled-canvas');
176
  const ctx = canvas.getContext('2d');
177
  const img = new Image();
178
-
179
- // Prevent right-click context menu on the canvas.
180
  canvas.oncontextmenu = (e) => { e.preventDefault(); return false; };
181
-
182
- // The image data is embedded directly here from the server.
183
  img.src = "data:image/png;base64,{{ unscrambled_base64 }}";
184
-
185
- img.onload = () => {
186
- // Draw the loaded image onto the canvas once it's ready.
187
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
188
- };
189
- img.onerror = () => {
190
- ctx.font = "16px Arial";
191
- ctx.fillStyle = "red";
192
- ctx.textAlign = "center";
193
- ctx.fillText("Error loading preview.", canvas.width / 2, canvas.height / 2);
194
- };
195
  </script>
196
  </body>
197
  </html>
@@ -202,65 +253,73 @@ RESULTS_PAGE_TEMPLATE = """
202
  @app.route('/')
203
  def home():
204
  """Renders the main upload page."""
205
- return render_template_string(HOME_PAGE_TEMPLATE, random_seed=random.randint(0, 99999))
206
 
207
  @app.route('/process', methods=['POST'])
208
  def process_image():
209
  """Handles the image processing."""
210
- if 'image_file' not in request.files:
211
- return render_template_string(HOME_PAGE_TEMPLATE, error="No file part in the request.")
212
-
213
- file = request.files['image_file']
214
- if file.filename == '':
215
- return render_template_string(HOME_PAGE_TEMPLATE, error="No file selected.")
216
-
217
- if file:
218
- try:
219
- grid_size = int(request.form.get('grid_size', 8))
220
- seed = int(request.form.get('seed', 12345))
221
-
222
- input_image = Image.open(file.stream).convert("RGB")
223
-
224
- # 1. Scramble the image
225
- scrambled_img, scramble_map = scramble_image(input_image, grid_size, seed)
226
- if scrambled_img is None:
227
- 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.")
228
-
229
- # 2. Create the map visualization
230
- map_viz_img = create_mapping_visualization(scramble_map, grid_size)
231
-
232
- # 3. Unscramble the image in memory for the canvas preview
233
- unscrambled_img = unscramble_image(scrambled_img, scramble_map, grid_size)
234
 
235
- # 4. Save downloadable files and get their URLs
236
- unique_id = uuid.uuid4()
237
- scrambled_filename = f"{unique_id}_scrambled.png"
238
- map_filename = f"{unique_id}_map.png"
239
- scrambled_img.save(os.path.join(app.config['UPLOAD_FOLDER'], scrambled_filename))
240
- map_viz_img.save(os.path.join(app.config['UPLOAD_FOLDER'], map_filename))
 
 
 
 
 
241
 
242
- # 5. Convert unscrambled image to Base64 for canvas
243
- unscrambled_base64 = pil_to_base64(unscrambled_img)
 
 
 
 
244
 
245
- # 6. Render the results page
246
- return render_template_string(
247
- RESULTS_PAGE_TEMPLATE,
248
- scrambled_image_url=url_for('get_image', filename=scrambled_filename),
249
- map_image_url=url_for('get_image', filename=map_filename),
250
- unscrambled_base64=unscrambled_base64,
251
- width=unscrambled_img.width,
252
- height=unscrambled_img.height
253
- )
 
254
 
255
- except Exception as e:
256
- return render_template_string(HOME_PAGE_TEMPLATE, error=f"An error occurred: {e}")
 
 
 
 
 
 
 
 
 
257
 
258
- return render_template_string(HOME_PAGE_TEMPLATE, error="An unknown error occurred.")
 
 
 
 
 
 
 
 
 
259
 
 
 
260
 
261
- @app.route('/temp_images/<filename>')
262
- def get_image(filename):
263
- """Serves the generated images from the temporary directory."""
264
  return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
265
 
266
  if __name__ == '__main__':
 
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
 
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">
 
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
  """
 
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>
 
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__':