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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -122
app.py CHANGED
@@ -1,142 +1,197 @@
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()
 
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()