Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import tempfile
|
4 |
+
from PIL import Image
|
5 |
+
import subprocess
|
6 |
+
import logging
|
7 |
+
import base64
|
8 |
+
import xml.etree.ElementTree as ET
|
9 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
10 |
+
import re
|
11 |
+
from iconsheet import icon_sheet_interface
|
12 |
+
import mimetypes
|
13 |
+
|
14 |
+
# Set up logging
|
15 |
+
logging.basicConfig(filename='svgmaker.log', level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(levelname)s - %(message)s')
|
17 |
+
|
18 |
+
def process_single_image(image_file, stroke_width, fill, stroke, opacity, output_size, output_dir):
|
19 |
+
try:
|
20 |
+
logging.info(f"Processing image: {image_file.name}")
|
21 |
+
with Image.open(image_file.name) as image:
|
22 |
+
# Calculate new dimensions maintaining aspect ratio
|
23 |
+
aspect_ratio = image.width / image.height
|
24 |
+
if aspect_ratio > 1:
|
25 |
+
new_width = output_size
|
26 |
+
new_height = int(output_size / aspect_ratio)
|
27 |
+
else:
|
28 |
+
new_height = output_size
|
29 |
+
new_width = int(output_size * aspect_ratio)
|
30 |
+
|
31 |
+
logging.info(f"Resizing image to {new_width}x{new_height}")
|
32 |
+
# Resize image
|
33 |
+
image = image.resize((new_width, new_height), Image.LANCZOS)
|
34 |
+
|
35 |
+
# Convert to grayscale
|
36 |
+
image = image.convert('L')
|
37 |
+
|
38 |
+
# Save as temporary PGM file
|
39 |
+
with tempfile.NamedTemporaryFile(suffix='.pgm', delete=False) as temp_pgm:
|
40 |
+
image.save(temp_pgm.name)
|
41 |
+
temp_pgm_path = temp_pgm.name
|
42 |
+
|
43 |
+
# Run potrace with improved settings
|
44 |
+
output_svg = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(image_file.name))[0]}.svg")
|
45 |
+
subprocess.run([
|
46 |
+
"potrace",
|
47 |
+
"--svg",
|
48 |
+
"--output", output_svg,
|
49 |
+
"--turdsize", "2",
|
50 |
+
"--alphamax", "1",
|
51 |
+
"--opttolerance", "0.2",
|
52 |
+
"--unit", "10",
|
53 |
+
temp_pgm_path
|
54 |
+
], check=True)
|
55 |
+
|
56 |
+
# Clean up temporary PGM
|
57 |
+
os.unlink(temp_pgm_path)
|
58 |
+
|
59 |
+
# Modify SVG with user-specified styles and remove unnecessary elements
|
60 |
+
with open(output_svg, 'r') as f:
|
61 |
+
svg_content = f.read()
|
62 |
+
|
63 |
+
# Remove namespace prefixes and metadata
|
64 |
+
svg_content = re.sub(r'<ns0:', '<', svg_content)
|
65 |
+
svg_content = re.sub(r'</ns0:', '</', svg_content)
|
66 |
+
svg_content = re.sub(r'xmlns:ns0="[^"]+"', '', svg_content)
|
67 |
+
svg_content = re.sub(r'<metadata>.*?</metadata>', '', svg_content, flags=re.DOTALL)
|
68 |
+
|
69 |
+
# Remove unnecessary group transformation
|
70 |
+
svg_content = re.sub(r'<g transform="[^"]+">', '<g>', svg_content)
|
71 |
+
|
72 |
+
# Update SVG attributes
|
73 |
+
svg_tree = ET.fromstring(svg_content)
|
74 |
+
svg_tree.set('width', str(new_width))
|
75 |
+
svg_tree.set('height', str(new_height))
|
76 |
+
svg_tree.set('viewBox', f"0 0 {new_width} {new_height}")
|
77 |
+
for path in svg_tree.findall('.//{http://www.w3.org/2000/svg}path'):
|
78 |
+
path.set('fill', fill)
|
79 |
+
path.set('fill-opacity', str(opacity))
|
80 |
+
path.set('stroke', stroke)
|
81 |
+
path.set('stroke-width', str(stroke_width))
|
82 |
+
path.set('stroke-opacity', str(opacity))
|
83 |
+
|
84 |
+
svg_content = ET.tostring(svg_tree, encoding='unicode')
|
85 |
+
|
86 |
+
# Final cleanup
|
87 |
+
svg_content = re.sub(r'\s+', ' ', svg_content) # Remove excess whitespace
|
88 |
+
svg_content = re.sub(r'> <', '><', svg_content) # Remove space between tags
|
89 |
+
|
90 |
+
with open(output_svg, 'w') as f:
|
91 |
+
f.write(svg_content)
|
92 |
+
|
93 |
+
# Create a base64 encoded version of the SVG for preview
|
94 |
+
svg_base64 = base64.b64encode(svg_content.encode('utf-8')).decode('utf-8')
|
95 |
+
preview = f"data:image/svg+xml;base64,{svg_base64}"
|
96 |
+
|
97 |
+
return output_svg, preview
|
98 |
+
except Exception as e:
|
99 |
+
logging.error(f"Error processing image {image_file.name}: {str(e)}")
|
100 |
+
return None, None
|
101 |
+
|
102 |
+
def vectorize_icons(images, stroke_width, fill, stroke, opacity, output_size, output_dir, progress=gr.Progress()):
|
103 |
+
vectorized_icons = []
|
104 |
+
svg_previews = []
|
105 |
+
total_images = len(images)
|
106 |
+
logging.info(f"Vectorizing {total_images} images")
|
107 |
+
|
108 |
+
os.makedirs(output_dir, exist_ok=True)
|
109 |
+
|
110 |
+
with ThreadPoolExecutor() as executor:
|
111 |
+
future_to_image = {executor.submit(process_single_image, image, stroke_width, fill, stroke, opacity, output_size, output_dir): image for image in images}
|
112 |
+
for i, future in enumerate(as_completed(future_to_image)):
|
113 |
+
image = future_to_image[future]
|
114 |
+
progress((i + 1) / total_images, f"Vectorizing image {i+1}/{total_images}")
|
115 |
+
try:
|
116 |
+
result, preview = future.result()
|
117 |
+
vectorized_icons.append(result)
|
118 |
+
svg_previews.append(preview)
|
119 |
+
logging.info(f"Successfully vectorized image {i+1}")
|
120 |
+
progress((i + 1) / total_images, f"Vectorized image {i+1}/{total_images}")
|
121 |
+
except Exception as e:
|
122 |
+
logging.error(f"Error vectorizing icon {image.name}: {str(e)}")
|
123 |
+
vectorized_icons.append(None)
|
124 |
+
svg_previews.append(None)
|
125 |
+
progress((i + 1) / total_images, f"Failed to vectorize image {i+1}/{total_images}: {str(e)}")
|
126 |
+
|
127 |
+
progress(1.0, "Vectorization complete")
|
128 |
+
logging.info(f"Vectorization complete. Successful: {len([i for i in vectorized_icons if i is not None])}, Failed: {len([i for i in vectorized_icons if i is None])}")
|
129 |
+
return vectorized_icons, svg_previews
|
130 |
+
|
131 |
+
def create_preview_grid(svg_previews):
|
132 |
+
# Dynamically create grid based on number of SVG previews
|
133 |
+
num_images = len([preview for preview in svg_previews if preview is not None])
|
134 |
+
|
135 |
+
# Define the number of columns for the grid
|
136 |
+
columns = 4
|
137 |
+
grid_html = f'<div style="display: grid; grid-template-columns: repeat({columns}, 1fr); gap: 10px; background-color: #f0f0f0; padding: 20px; border-radius: 10px;">'
|
138 |
+
|
139 |
+
for preview in svg_previews:
|
140 |
+
if preview:
|
141 |
+
grid_html += f'''
|
142 |
+
<div style="background-color: white; padding: 10px; border-radius: 5px;">
|
143 |
+
<img src="{preview}" style="width: 100%; height: auto;" onclick="this.classList.toggle('zoomed')" class="zoomable">
|
144 |
+
</div>
|
145 |
+
'''
|
146 |
+
else:
|
147 |
+
grid_html += '<div style="background-color: white; padding: 10px; border-radius: 5px;"><p>Error</p></div>'
|
148 |
+
|
149 |
+
grid_html += '</div>'
|
150 |
+
|
151 |
+
# Add CSS for zoom functionality
|
152 |
+
grid_html += '''
|
153 |
+
<style>
|
154 |
+
.zoomable { transition: transform 0.3s ease; }
|
155 |
+
.zoomable.zoomed { transform: scale(2.5); z-index: 1000; position: relative; }
|
156 |
+
</style>
|
157 |
+
'''
|
158 |
+
|
159 |
+
return grid_html
|
160 |
+
|
161 |
+
|
162 |
+
with gr.Blocks() as app:
|
163 |
+
gr.Markdown("# SVG Maker and Icon Sheet Generator")
|
164 |
+
|
165 |
+
with gr.Tabs():
|
166 |
+
with gr.TabItem("SVG Maker"):
|
167 |
+
gr.Markdown("# SVG Maker")
|
168 |
+
|
169 |
+
with gr.Row():
|
170 |
+
image_input = gr.File(label="Upload Images", file_count="multiple")
|
171 |
+
|
172 |
+
with gr.Row():
|
173 |
+
stroke_width = gr.Slider(0, 10, label="Stroke Width", value=1)
|
174 |
+
fill = gr.ColorPicker(label="Fill Color", value="#000000")
|
175 |
+
stroke = gr.ColorPicker(label="Stroke Color", value="#000000")
|
176 |
+
opacity = gr.Slider(0, 1, label="Opacity", value=1, step=0.1)
|
177 |
+
|
178 |
+
with gr.Row():
|
179 |
+
output_size = gr.Slider(32, 1024, label="Output Size (px)", value=512, step=32)
|
180 |
+
output_dir = gr.Textbox(label="Output Directory", value="output", placeholder="Enter output directory path")
|
181 |
+
|
182 |
+
vectorize_btn = gr.Button("Vectorize")
|
183 |
+
svg_output = gr.File(label="Vectorized Icons", file_count="multiple")
|
184 |
+
svg_preview = gr.HTML(label="SVG Previews")
|
185 |
+
|
186 |
+
def process_and_display(images, stroke_width, fill, stroke, opacity, output_size, output_dir):
|
187 |
+
vectorized_icons, svg_previews = vectorize_icons(images, stroke_width, fill, stroke, opacity, output_size, output_dir)
|
188 |
+
preview_html = create_preview_grid(svg_previews)
|
189 |
+
# Filter out None values from vectorized_icons
|
190 |
+
valid_icons = [icon for icon in vectorized_icons if icon is not None]
|
191 |
+
return valid_icons, preview_html
|
192 |
+
|
193 |
+
vectorize_btn.click(process_and_display,
|
194 |
+
inputs=[image_input, stroke_width, fill, stroke, opacity, output_size, output_dir],
|
195 |
+
outputs=[svg_output, svg_preview])
|
196 |
+
|
197 |
+
with gr.TabItem("Icon Sheet"):
|
198 |
+
mimetypes.add_type('image/svg+xml', '.svg')
|
199 |
+
icon_sheet_interface()
|
200 |
+
|
201 |
+
if __name__ == "__main__":
|
202 |
+
try:
|
203 |
+
app.launch()
|
204 |
+
except Exception as e:
|
205 |
+
logging.error(f"Failed to launch app: {str(e)}")
|
206 |
+
raise
|