broadfield-dev commited on
Commit
dc362d8
·
verified ·
1 Parent(s): 35f0f0d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -179
app.py CHANGED
@@ -1,86 +1,60 @@
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
 
8
- # --- Core Image Processing Logic ---
9
 
10
  def process_image_for_grid(image_pil, grid_size):
11
- """Crops the image to be perfectly divisible by the grid size."""
12
- if image_pil is None:
13
- return None
14
  img_width, img_height = image_pil.size
15
- tile_w = img_width // grid_size
16
- tile_h = img_height // grid_size
17
-
18
- # If image is too small for the grid, return None to signal an error
19
- if tile_w == 0 or tile_h == 0:
20
- 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.")
21
-
22
- # Crop the image
23
  cropped_image = image_pil.crop((0, 0, tile_w * grid_size, tile_h * grid_size))
24
  return cropped_image, tile_w, tile_h
25
 
26
  def scramble_image(image_pil, grid_size, seed):
27
- """Scrambles an image by dividing it into a grid and shuffling the tiles based on a seed."""
28
  cropped_image, tile_w, tile_h = process_image_for_grid(image_pil, grid_size)
29
-
30
- tiles = []
31
- for i in range(grid_size):
32
- for j in range(grid_size):
33
- box = (j * tile_w, i * tile_h, (j + 1) * tile_w, (i + 1) * tile_h)
34
- tiles.append(cropped_image.crop(box))
35
-
36
- num_tiles = grid_size * grid_size
37
-
38
- # Use a seeded random generator for reproducible results
39
  rng = np.random.default_rng(seed=int(seed))
40
- scramble_map = rng.permutation(num_tiles)
41
-
42
  scrambled_image = Image.new('RGB', cropped_image.size)
43
  for new_pos_index, original_tile_index in enumerate(scramble_map):
44
- tile_to_place = tiles[original_tile_index]
45
  i, j = divmod(new_pos_index, grid_size)
46
- scrambled_image.paste(tile_to_place, (j * tile_w, i * tile_h))
47
-
48
  return scrambled_image, scramble_map
49
 
50
  def unscramble_image(scrambled_pil, scramble_map, grid_size):
51
- """Unscrambles an image using the provided scramble_map."""
52
  cropped_image, tile_w, tile_h = process_image_for_grid(scrambled_pil, grid_size)
53
-
54
- scrambled_tiles = []
55
- for i in range(grid_size):
56
- for j in range(grid_size):
57
- box = (j * tile_w, i * tile_h, (j + 1) * tile_w, (i + 1) * tile_h)
58
- scrambled_tiles.append(cropped_image.crop(box))
59
-
60
- num_tiles = grid_size * grid_size
61
  unscrambled_image = Image.new('RGB', cropped_image.size)
62
-
63
- # The scramble_map tells us: new_position -> original_position
64
- # We need to place each scrambled tile back to its original spot.
65
  for new_pos_index, original_pos_index in enumerate(scramble_map):
66
- tile_to_place = scrambled_tiles[new_pos_index]
67
  i, j = divmod(original_pos_index, grid_size)
68
- unscrambled_image.paste(tile_to_place, (j * tile_w, i * tile_h))
69
-
70
  return unscrambled_image
71
 
72
-
73
- # --- Visualization and HTML/JS Logic ---
74
-
75
- def create_mapping_visualization(scramble_map, grid_size, map_size=(512, 512)):
76
- """Creates an image that visualizes the scrambling map."""
77
  vis_image = Image.new('RGB', map_size, color='lightgray')
78
  draw = ImageDraw.Draw(vis_image)
79
- try:
80
- font = ImageFont.truetype("arial.ttf", size=max(10, 32 - grid_size * 2))
81
- except IOError:
82
- font = ImageFont.load_default()
83
-
84
  tile_w, tile_h = map_size[0] // grid_size, map_size[1] // grid_size
85
  for new_pos_index, original_pos_index in enumerate(scramble_map):
86
  i, j = divmod(new_pos_index, grid_size)
@@ -93,142 +67,201 @@ def create_mapping_visualization(scramble_map, grid_size, map_size=(512, 512)):
93
  return vis_image
94
 
95
  def pil_to_base64(pil_image):
96
- """Converts a PIL Image to a Base64 string for HTML embedding."""
97
  buffered = io.BytesIO()
98
  pil_image.save(buffered, format="PNG")
99
- img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
100
- return f"data:image/png;base64,{img_str}"
101
-
102
- def create_canvas_html(base64_string, width, height):
103
- """Creates the HTML and JavaScript to render an image on a canvas."""
104
- # The style disables right-click context menu, making it harder to save.
105
- html_content = f"""
106
- <div style="display: flex; justify-content: center; align-items: center; border: 1px solid #ccc; background: #f0f0f0;">
107
- <canvas id="unscrambled-canvas" width="{width}" height="{height}" style="max-width: 100%; max-height: 512px; object-fit: contain;"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  <script>
110
- // Use a function to avoid polluting the global scope
111
- function drawImageOnCanvas() {{
112
- const canvas = document.getElementById('unscrambled-canvas');
113
- if (!canvas) return; // Exit if canvas not found
114
- const ctx = canvas.getContext('2d');
115
- const img = new Image();
116
- img.src = "{base64_string}";
117
- img.onload = () => {{
118
- // Clear canvas before drawing new image
119
- ctx.clearRect(0, 0, canvas.width, canvas.height);
120
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
121
- }};
122
- img.onerror = () => {{
123
- ctx.font = "16px Arial";
124
- ctx.fillStyle = "red";
125
- ctx.textAlign = "center";
126
- ctx.fillText("Error loading image.", canvas.width / 2, canvas.height / 2);
127
- }};
128
- // Prevent right-click on the canvas to make saving harder
129
- canvas.oncontextmenu = (e) => {{ e.preventDefault(); return false; }};
130
- }}
131
- // Ensure this runs after the Gradio component is mounted
132
- setTimeout(drawImageOnCanvas, 100);
133
  </script>
134
- """
135
- return html_content
 
136
 
137
- # --- Main Gradio Function ---
138
 
139
- def process_and_display(input_image, grid_size, seed):
140
- """The main orchestrator function for the Gradio app."""
141
- if input_image is None:
142
- return None, None, "<div>Please upload an image to begin.</div>"
143
 
144
- # 1. Scramble the image
145
- scrambled_img, scramble_map = scramble_image(input_image, grid_size, seed)
146
-
147
- # 2. Create the visualization of the map
148
- map_viz_img = create_mapping_visualization(scramble_map, grid_size)
149
 
150
- # 3. Unscramble the image in memory (this is never sent as a downloadable file)
151
- unscrambled_img = unscramble_image(scrambled_img, scramble_map, grid_size)
152
-
153
- # 4. Convert the unscrambled image to Base64 and create HTML for the canvas
154
- base64_unscrambled = pil_to_base64(unscrambled_img)
155
- canvas_html = create_canvas_html(base64_unscrambled, unscrambled_img.width, unscrambled_img.height)
156
 
157
- return scrambled_img, map_viz_img, canvas_html
 
 
 
 
 
 
 
 
 
 
158
 
 
 
 
 
 
159
 
160
- # --- Gradio UI Definition ---
 
 
 
 
 
161
 
162
- css = """
163
- #output_container { max-height: 540px; }
164
- footer { display: none !important; }
165
- """
166
 
167
- with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
168
- gr.Markdown(
169
- """
170
- # 🖼️ Secure Image Scrambler & Viewer
171
- This tool scrambles an image so you can share it securely. Only the scrambled version is downloadable.
172
- You can then view the **unscrambled** original in a special preview window that prevents easy downloading.
173
- """
174
- )
175
-
176
- with gr.Row():
177
- with gr.Column(scale=1, min_width=350):
178
- input_image = gr.Image(
179
- type="pil",
180
- label="Upload Image or Paste from Clipboard/URL",
181
- sources=["upload", "clipboard"]
182
- )
183
-
184
- with gr.Accordion("Settings", open=True):
185
- grid_size_slider = gr.Slider(
186
- minimum=2, maximum=32, value=8, step=1, label="Grid Size (NxN)"
187
- )
188
- with gr.Row():
189
- seed_input = gr.Number(
190
- value=lambda: random.randint(0, 99999), label="Scramble Seed"
191
- )
192
- random_seed_btn = gr.Button("🎲")
193
- random_seed_btn.click(
194
- fn=lambda: random.randint(0, 99999),
195
- inputs=[],
196
- outputs=seed_input
197
- )
198
-
199
- submit_btn = gr.Button("Scramble & Process", variant="primary")
200
- gr.Examples(
201
- examples=[
202
- ["./examples/cheetah.jpg", 12, 12345],
203
- ["./examples/city.jpg", 8, 54321],
204
- ["./examples/parrot.jpg", 16, 9876]
205
- ],
206
- inputs=[input_image, grid_size_slider, seed_input],
207
- fn=process_and_display,
208
  )
209
 
210
- with gr.Column(scale=2):
211
- with gr.Tabs(elem_id="output_container"):
212
- with gr.TabItem("Scrambled Image (Downloadable)"):
213
- scrambled_output = gr.Image(
214
- label="Scrambled Image (Right-click to Save)",
215
- interactive=False,
216
- )
217
- with gr.TabItem("Unscrambled Preview (Protected)"):
218
- unscrambled_canvas = gr.HTML(
219
- label="Unscrambled Preview (Not directly downloadable)"
220
- )
221
- with gr.TabItem("Scrambling Map"):
222
- mapping_output = gr.Image(
223
- label="Mapping Key (Where each piece came from)",
224
- interactive=False,
225
- )
226
-
227
- # Connect the button to the main function
228
- submit_btn.click(
229
- fn=process_and_display,
230
- inputs=[input_image, grid_size_slider, seed_input],
231
- outputs=[scrambled_output, mapping_output, unscrambled_canvas]
232
- )
233
-
234
- demo.launch()
 
1
+ import os
2
+ 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
25
+ tile_w, tile_h = img_width // grid_size, img_height // grid_size
26
+ if tile_w == 0 or tile_h == 0: return None, None, None
 
 
 
 
 
 
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
  cropped_image, tile_w, tile_h = process_image_for_grid(image_pil, grid_size)
32
+ if not cropped_image: return None, None
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
  cropped_image, tile_w, tile_h = process_image_for_grid(scrambled_pil, grid_size)
44
+ if not cropped_image: return None
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
+ map_size=(512, 512)
 
 
 
54
  vis_image = Image.new('RGB', map_size, color='lightgray')
55
  draw = ImageDraw.Draw(vis_image)
56
+ try: font = ImageFont.truetype("DejaVuSans.ttf", size=max(10, 32 - grid_size * 2))
57
+ except IOError: font = ImageFont.load_default()
 
 
 
58
  tile_w, tile_h = map_size[0] // grid_size, map_size[1] // grid_size
59
  for new_pos_index, original_pos_index in enumerate(scramble_map):
60
  i, j = divmod(new_pos_index, grid_size)
 
67
  return vis_image
68
 
69
  def pil_to_base64(pil_image):
 
70
  buffered = io.BytesIO()
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">
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
+ 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
+ """
122
+
123
+ RESULTS_PAGE_TEMPLATE = """
124
+ <!DOCTYPE html>
125
+ <html lang="en">
126
+ <head>
127
+ <meta charset="UTF-8">
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>
198
+ """
199
 
200
+ # --- Flask Routes ---
201
 
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__':
267
+ app.run(host=HOST, port=PORT)