313 lines
12 KiB
Python
313 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
svg_to_png_gui.py
|
||
|
||
A standalone Python/Tkinter app to batch-convert SVG files to PNG using Inkscape.
|
||
|
||
Key Features:
|
||
-------------
|
||
1. Modern GUI (Tkinter).
|
||
2. Multiple File Selection:
|
||
- Add individual SVG files.
|
||
- Add entire folders containing SVG files (recursive).
|
||
3. Real-time Logging:
|
||
- Each conversion step logs info in the GUI.
|
||
- Logs shown for both success and failure.
|
||
4. Progress Bar and Statistics:
|
||
- Shows progress while converting multiple files.
|
||
- At the end, displays success/failure counts.
|
||
5. Customizable:
|
||
- DPI setting.
|
||
- Background color.
|
||
6. Automatic Inkscape Detection:
|
||
- Checks common installation paths.
|
||
- Allows manual override if not found.
|
||
7. Fully Cross-Platform:
|
||
- Windows, Mac, Linux (requires installed Inkscape).
|
||
8. Easy Packaging:
|
||
- No external dependencies.
|
||
- PyInstaller can build a single .exe or .app.
|
||
|
||
Author: ChatGPT
|
||
Author: Ryan Schultz
|
||
Author: Regis Nde Tene
|
||
"""
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
|
||
class SVGtoPNGConverter:
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("SVG to PNG Converter")
|
||
self.root.geometry("600x450")
|
||
self.root.resizable(False, False)
|
||
|
||
# -------------- Variables --------------
|
||
self.selected_files = []
|
||
self.dpi_var = tk.StringVar(value="150")
|
||
self.bg_color_var = tk.StringVar(value="white")
|
||
self.inkscape_path_var = tk.StringVar(value=self.find_inkscape_path() or "")
|
||
self.success_count = 0
|
||
self.fail_count = 0
|
||
|
||
# -------------- Main Frames --------------
|
||
self.create_top_frame()
|
||
self.create_center_frame()
|
||
self.create_bottom_frame()
|
||
|
||
def create_top_frame(self):
|
||
""" Create the top frame with add files/folders and Inkscape path. """
|
||
top_frame = ttk.Frame(self.root, padding=(10, 10))
|
||
top_frame.pack(fill=tk.X)
|
||
|
||
# Add File Button
|
||
add_file_btn = ttk.Button(top_frame, text="Add SVG File(s)", command=self.add_files)
|
||
add_file_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
# Add Folder Button
|
||
add_folder_btn = ttk.Button(top_frame, text="Add Folder", command=self.add_folder)
|
||
add_folder_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
# Inkscape Path
|
||
inkscape_label = ttk.Label(top_frame, text="Inkscape Path:")
|
||
inkscape_label.pack(side=tk.LEFT, padx=(30, 2))
|
||
|
||
inkscape_entry = ttk.Entry(top_frame, textvariable=self.inkscape_path_var, width=35)
|
||
inkscape_entry.pack(side=tk.LEFT, padx=5)
|
||
|
||
browse_inkscape_btn = ttk.Button(top_frame, text="Browse...", command=self.browse_inkscape)
|
||
browse_inkscape_btn.pack(side=tk.LEFT, padx=(0,5))
|
||
|
||
def create_center_frame(self):
|
||
""" Create the center frame with a file list, config, and logging area. """
|
||
center_frame = ttk.Frame(self.root, padding=(10, 0, 10, 10))
|
||
center_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# -- File List --
|
||
file_list_frame = ttk.LabelFrame(center_frame, text="Selected Files")
|
||
file_list_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10), pady=5)
|
||
|
||
self.file_listbox = tk.Listbox(file_list_frame, width=35, height=15, selectmode=tk.EXTENDED)
|
||
self.file_listbox.pack(side=tk.LEFT, fill=tk.Y)
|
||
|
||
file_list_scroll = ttk.Scrollbar(file_list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
|
||
file_list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.file_listbox.config(yscrollcommand=file_list_scroll.set)
|
||
|
||
# -- Settings Panel --
|
||
settings_frame = ttk.LabelFrame(center_frame, text="Conversion Settings")
|
||
settings_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
||
|
||
# DPI
|
||
dpi_label = ttk.Label(settings_frame, text="DPI:")
|
||
dpi_label.grid(row=0, column=0, padx=5, pady=5, sticky=tk.E)
|
||
dpi_entry = ttk.Entry(settings_frame, textvariable=self.dpi_var, width=8)
|
||
dpi_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
|
||
|
||
# Background Color
|
||
bg_label = ttk.Label(settings_frame, text="Background:")
|
||
bg_label.grid(row=1, column=0, padx=5, pady=5, sticky=tk.E)
|
||
bg_entry = ttk.Entry(settings_frame, textvariable=self.bg_color_var, width=8)
|
||
bg_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
|
||
|
||
# Progress Bar
|
||
self.progress = ttk.Progressbar(settings_frame, orient='horizontal', length=180, mode='determinate')
|
||
self.progress.grid(row=0, column=2, rowspan=2, padx=30, pady=5)
|
||
|
||
# -- Logging Panel --
|
||
log_frame = ttk.LabelFrame(center_frame, text="Conversion Log")
|
||
log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
|
||
|
||
self.log_text = tk.Text(log_frame, height=8, width=60, state=tk.DISABLED)
|
||
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
log_scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
|
||
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.log_text.config(yscrollcommand=log_scrollbar.set)
|
||
|
||
def create_bottom_frame(self):
|
||
""" Create the bottom frame with Convert, Remove, and Clear buttons. """
|
||
bottom_frame = ttk.Frame(self.root, padding=(10, 10))
|
||
bottom_frame.pack(fill=tk.X)
|
||
|
||
remove_btn = ttk.Button(bottom_frame, text="Remove Selected", command=self.remove_selected)
|
||
remove_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
clear_btn = ttk.Button(bottom_frame, text="Clear All", command=self.clear_all)
|
||
clear_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
convert_btn = ttk.Button(bottom_frame, text="Convert to PNG", command=self.start_conversion)
|
||
convert_btn.pack(side=tk.RIGHT, padx=5)
|
||
|
||
def add_files(self):
|
||
""" Open file dialog for multiple SVG files and add them. """
|
||
filepaths = filedialog.askopenfilenames(
|
||
title="Select SVG File(s)",
|
||
filetypes=[("SVG Files", "*.svg"), ("All Files", "*.*")]
|
||
)
|
||
if not filepaths:
|
||
return
|
||
for f in filepaths:
|
||
if f not in self.selected_files:
|
||
self.selected_files.append(f)
|
||
self.file_listbox.insert(tk.END, f)
|
||
|
||
def add_folder(self):
|
||
""" Open directory dialog and add all SVG files in that folder (recursively). """
|
||
folder_path = filedialog.askdirectory(title="Select Folder with SVG Files")
|
||
if not folder_path:
|
||
return
|
||
|
||
for root, dirs, files in os.walk(folder_path):
|
||
for fname in files:
|
||
if fname.lower().endswith(".svg"):
|
||
full_path = os.path.join(root, fname)
|
||
if full_path not in self.selected_files:
|
||
self.selected_files.append(full_path)
|
||
self.file_listbox.insert(tk.END, full_path)
|
||
|
||
def remove_selected(self):
|
||
""" Remove selected files from the listbox and internal list. """
|
||
selected_indices = list(self.file_listbox.curselection())
|
||
selected_indices.reverse() # Remove from the end to not mess up indices
|
||
for index in selected_indices:
|
||
self.selected_files.pop(index)
|
||
self.file_listbox.delete(index)
|
||
|
||
def clear_all(self):
|
||
""" Clear the entire list of selected files. """
|
||
self.selected_files.clear()
|
||
self.file_listbox.delete(0, tk.END)
|
||
|
||
def browse_inkscape(self):
|
||
""" Let user manually select Inkscape executable if auto-detection fails. """
|
||
path = filedialog.askopenfilename(title="Select Inkscape Executable")
|
||
if path:
|
||
self.inkscape_path_var.set(path)
|
||
|
||
def find_inkscape_path(self):
|
||
"""
|
||
Try to auto-detect Inkscape in common locations or in PATH.
|
||
Return the path if found, else None.
|
||
"""
|
||
possible_locations = []
|
||
# Windows common installations
|
||
if os.name == 'nt':
|
||
possible_locations += [
|
||
r"C:\Program Files\Inkscape\bin\inkscape.exe",
|
||
r"C:\Program Files\Inkscape\inkscape.exe",
|
||
r"C:\Program Files (x86)\Inkscape\inkscape.exe",
|
||
]
|
||
# macOS brew / Applications
|
||
possible_locations += [
|
||
"/Applications/Inkscape.app/Contents/MacOS/inkscape",
|
||
"/opt/homebrew/bin/inkscape",
|
||
"/usr/local/bin/inkscape",
|
||
"/usr/bin/inkscape",
|
||
]
|
||
# Also check PATH
|
||
path_env = os.environ.get("PATH", "")
|
||
for p in path_env.split(os.pathsep):
|
||
possible_locations.append(os.path.join(p, "inkscape"))
|
||
possible_locations.append(os.path.join(p, "inkscape.exe"))
|
||
|
||
# Return first location that exists
|
||
for loc in possible_locations:
|
||
if os.path.isfile(loc) and os.access(loc, os.X_OK):
|
||
return loc
|
||
return None
|
||
|
||
def log(self, message, level="info"):
|
||
""" Append a log message to the text box. """
|
||
self.log_text.config(state=tk.NORMAL)
|
||
self.log_text.insert(tk.END, f"{message}\n")
|
||
self.log_text.see(tk.END)
|
||
self.log_text.config(state=tk.DISABLED)
|
||
|
||
def start_conversion(self):
|
||
""" Validate input and begin conversion process. """
|
||
if not self.selected_files:
|
||
messagebox.showerror("Error", "No SVG files selected.")
|
||
return
|
||
|
||
inkscape_path = self.inkscape_path_var.get().strip()
|
||
if not inkscape_path or not os.path.exists(inkscape_path):
|
||
messagebox.showerror("Error", "Inkscape executable not found.\nPlease specify the correct path.")
|
||
return
|
||
|
||
# Check DPI
|
||
dpi_val = self.dpi_var.get().strip()
|
||
if not dpi_val.isdigit():
|
||
messagebox.showerror("Error", "DPI must be a positive integer.")
|
||
return
|
||
|
||
# Let’s do conversions in the main thread (simple approach).
|
||
# For big conversions, you might want to use threading or multiprocessing.
|
||
self.success_count = 0
|
||
self.fail_count = 0
|
||
|
||
self.log("Starting conversion...")
|
||
self.progress['value'] = 0
|
||
total_files = len(self.selected_files)
|
||
increment = 100 / total_files
|
||
|
||
for i, svg_file in enumerate(self.selected_files, start=1):
|
||
# Try converting
|
||
result = self.convert_file(
|
||
inkscape_path=inkscape_path,
|
||
svg_file=svg_file,
|
||
dpi=dpi_val,
|
||
bg_color=self.bg_color_var.get().strip(),
|
||
)
|
||
|
||
# Update progress bar
|
||
self.progress['value'] += increment
|
||
self.root.update_idletasks()
|
||
|
||
# Final stats
|
||
self.log(f"\nConversion finished. Success: {self.success_count}, Fail: {self.fail_count}")
|
||
self.progress['value'] = 100
|
||
|
||
def convert_file(self, inkscape_path, svg_file, dpi, bg_color):
|
||
""" Convert one SVG file to PNG using Inkscape. """
|
||
base, ext = os.path.splitext(svg_file)
|
||
output_png = base + ".png"
|
||
|
||
msg = f"Converting '{svg_file}' -> '{output_png}'"
|
||
self.log(msg)
|
||
|
||
cmd = [
|
||
inkscape_path,
|
||
svg_file,
|
||
"--export-type=png",
|
||
f"--export-filename={output_png}",
|
||
f"--export-dpi={dpi}",
|
||
f"--export-background={bg_color}"
|
||
]
|
||
|
||
try:
|
||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||
if os.path.exists(output_png):
|
||
self.log(" [OK] Conversion successful.")
|
||
self.success_count += 1
|
||
return True
|
||
else:
|
||
self.log(" [ERROR] Output PNG not found after conversion.", level="error")
|
||
self.fail_count += 1
|
||
return False
|
||
except subprocess.CalledProcessError as e:
|
||
self.log(f" [ERROR] Conversion failed for '{svg_file}'\n {e}", level="error")
|
||
self.fail_count += 1
|
||
return False
|
||
|
||
|
||
def main():
|
||
root = tk.Tk()
|
||
app = SVGtoPNGConverter(root)
|
||
root.mainloop()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|