Utility_Apps/Svg2PngPdf/Archived/SVG to PNG-PDF Converter-D.py

528 lines
No EOL
21 KiB
Python

#!/usr/bin/env python3
import os
import sys
import subprocess
import platform
import json
import threading
from pathlib import Path
from tkinter import *
from tkinter import ttk, filedialog, messagebox, scrolledtext
from tkinterdnd2 import TkinterDnD, DND_FILES
from PIL import Image, ImageTk
import darkdetect
class SVGtoPNGConverter(TkinterDnD.Tk):
def __init__(self):
super().__init__()
self.title("SVG to PNG Converter")
self.geometry("900x700")
self.minsize(800, 600)
# Initialize variables
self.selected_files = []
self.failed_files = []
self.conversion_running = False
self.dark_mode = darkdetect.isDark() if hasattr(darkdetect, 'isDark') else False
# Load settings
self.settings_file = Path.home() / ".svg2png_settings.json"
self.load_settings()
# Setup UI
self.setup_ui()
# Check Inkscape availability
self.inkscape_path = self.find_inkscape()
if not self.inkscape_path:
self.log("Warning: Inkscape not found in PATH or common locations. Conversion will not work until Inkscape is installed.", "warning")
# Configure window close behavior
self.protocol("WM_DELETE_WINDOW", self.on_close)
def setup_ui(self):
"""Setup the main user interface"""
# Configure style
self.style = ttk.Style()
self.update_theme()
# Main container
main_frame = ttk.Frame(self)
main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# Top panel - File selection
file_frame = ttk.LabelFrame(main_frame, text="Files to Convert", padding=10)
file_frame.pack(fill=X, pady=(0, 10))
# File list with scrollbar
self.file_list = Listbox(file_frame, selectmode=MULTIPLE, height=8)
self.file_list.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar = ttk.Scrollbar(file_frame, orient=VERTICAL, command=self.file_list.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.file_list.config(yscrollcommand=scrollbar.set)
# File controls
file_controls = ttk.Frame(file_frame)
file_controls.pack(side=RIGHT, fill=Y, padx=(10, 0))
ttk.Button(file_controls, text="Add Files", command=self.add_files).pack(fill=X, pady=2)
ttk.Button(file_controls, text="Add Folder", command=self.add_folder).pack(fill=X, pady=2)
ttk.Button(file_controls, text="Remove", command=self.remove_files).pack(fill=X, pady=2)
ttk.Button(file_controls, text="Clear All", command=self.clear_files).pack(fill=X, pady=2)
# Enable drag and drop
self.drop_target_register(DND_FILES)
self.dnd_bind('<<Drop>>', self.handle_drop)
# Middle panel - Settings
settings_frame = ttk.LabelFrame(main_frame, text="Conversion Settings", padding=10)
settings_frame.pack(fill=X, pady=(0, 10))
# DPI setting
ttk.Label(settings_frame, text="DPI:").grid(row=0, column=0, sticky=W, padx=(0, 5))
self.dpi_var = IntVar(value=self.settings.get('dpi', 96))
ttk.Spinbox(settings_frame, from_=72, to=600, increment=24, textvariable=self.dpi_var, width=6).grid(row=0, column=1, sticky=W)
# Background color
ttk.Label(settings_frame, text="Background:").grid(row=0, column=2, sticky=W, padx=(10, 5))
self.bg_var = StringVar(value=self.settings.get('background', '#FFFFFF'))
bg_frame = ttk.Frame(settings_frame)
bg_frame.grid(row=0, column=3, sticky=W)
ttk.Entry(bg_frame, textvariable=self.bg_var, width=10).pack(side=LEFT)
ttk.Button(bg_frame, text="Pick", command=self.pick_color, width=5).pack(side=LEFT, padx=(5, 0))
# Output format
ttk.Label(settings_frame, text="Format:").grid(row=0, column=4, sticky=W, padx=(10, 5))
self.format_var = StringVar(value=self.settings.get('format', 'png'))
ttk.Combobox(settings_frame, textvariable=self.format_var, values=['png', 'jpg', 'pdf', 'tiff'], width=6).grid(row=0, column=5, sticky=W)
# Output directory
ttk.Label(settings_frame, text="Output:").grid(row=1, column=0, sticky=W, padx=(0, 5), pady=(10, 0))
self.output_var = StringVar(value=self.settings.get('output_dir', 'Same as source'))
output_frame = ttk.Frame(settings_frame)
output_frame.grid(row=1, column=1, columnspan=4, sticky=EW, pady=(10, 0))
ttk.Entry(output_frame, textvariable=self.output_var).pack(side=LEFT, fill=X, expand=True)
ttk.Button(output_frame, text="Browse", command=self.choose_output_dir, width=8).pack(side=LEFT, padx=(5, 0))
# Additional options
options_frame = ttk.Frame(settings_frame)
options_frame.grid(row=2, column=0, columnspan=6, sticky=W, pady=(10, 0))
self.overwrite_var = BooleanVar(value=self.settings.get('overwrite', False))
ttk.Checkbutton(options_frame, text="Overwrite existing", variable=self.overwrite_var).pack(side=LEFT, padx=(0, 10))
self.auto_open_var = BooleanVar(value=self.settings.get('auto_open', False))
ttk.Checkbutton(options_frame, text="Open output folder", variable=self.auto_open_var).pack(side=LEFT)
# Bottom panel - Log and controls
log_frame = ttk.LabelFrame(main_frame, text="Conversion Log", padding=10)
log_frame.pack(fill=BOTH, expand=True)
self.log_area = scrolledtext.ScrolledText(log_frame, wrap=WORD, state='normal', height=10)
self.log_area.pack(fill=BOTH, expand=True)
# Configure log tags for colored text
self.log_area.tag_config("info", foreground="blue" if not self.dark_mode else "lightblue")
self.log_area.tag_config("success", foreground="green" if not self.dark_mode else "lightgreen")
self.log_area.tag_config("warning", foreground="orange" if not self.dark_mode else "yellow")
self.log_area.tag_config("error", foreground="red" if not self.dark_mode else "pink")
# Progress bar
self.progress_var = DoubleVar()
self.progress_bar = ttk.Progressbar(log_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(fill=X, pady=(5, 0))
# Status bar
self.status_var = StringVar(value="Ready")
status_bar = ttk.Label(log_frame, textvariable=self.status_var, relief=SUNKEN)
status_bar.pack(fill=X, pady=(5, 0))
# Control buttons
control_frame = ttk.Frame(main_frame)
control_frame.pack(fill=X, pady=(10, 0))
ttk.Button(control_frame, text="Convert", command=self.start_conversion).pack(side=LEFT, padx=(0, 10))
ttk.Button(control_frame, text="Retry Failed", command=self.retry_failed).pack(side=LEFT, padx=(0, 10))
ttk.Button(control_frame, text="Clear Log", command=self.clear_log).pack(side=LEFT)
# Theme toggle button
theme_btn = ttk.Button(control_frame, text="☀️" if self.dark_mode else "🌙", command=self.toggle_theme, width=3)
theme_btn.pack(side=RIGHT)
# Configure grid weights for resizing
settings_frame.columnconfigure(1, weight=1)
settings_frame.columnconfigure(3, weight=1)
def update_theme(self):
"""Update the theme based on dark mode setting"""
if self.dark_mode:
self.style.theme_use('alt')
self.configure(background='#2d2d2d')
self.log_area.configure(bg='#2d2d2d', fg='white', insertbackground='white')
else:
self.style.theme_use('clam')
self.configure(background='SystemButtonFace')
self.log_area.configure(bg='white', fg='black', insertbackground='black')
def toggle_theme(self):
"""Toggle between dark and light mode"""
self.dark_mode = not self.dark_mode
self.update_theme()
self.settings['dark_mode'] = self.dark_mode
self.save_settings()
def load_settings(self):
"""Load settings from file"""
default_settings = {
'dpi': 96,
'background': '#FFFFFF',
'format': 'png',
'output_dir': 'Same as source',
'overwrite': False,
'auto_open': False,
'dark_mode': self.dark_mode
}
try:
if self.settings_file.exists():
with open(self.settings_file, 'r') as f:
self.settings = {**default_settings, **json.load(f)}
else:
self.settings = default_settings
except Exception as e:
self.log(f"Error loading settings: {str(e)}", "error")
self.settings = default_settings
def save_settings(self):
"""Save settings to file"""
self.settings.update({
'dpi': self.dpi_var.get(),
'background': self.bg_var.get(),
'format': self.format_var.get(),
'output_dir': self.output_var.get(),
'overwrite': self.overwrite_var.get(),
'auto_open': self.auto_open_var.get(),
'dark_mode': self.dark_mode
})
try:
with open(self.settings_file, 'w') as f:
json.dump(self.settings, f, indent=2)
except Exception as e:
self.log(f"Error saving settings: {str(e)}", "error")
def find_inkscape(self):
"""Find Inkscape executable in common locations"""
# Check if Inkscape is in PATH
try:
if platform.system() == "Windows":
result = subprocess.run(["where", "inkscape"], capture_output=True, text=True)
else:
result = subprocess.run(["which", "inkscape"], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.splitlines()[0].strip()
except:
pass
# Check common installation paths
common_paths = []
if platform.system() == "Windows":
program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
common_paths.extend([
os.path.join(program_files, "Inkscape", "bin", "inkscape.exe"),
os.path.join(program_files, "Inkscape", "inkscape.exe"),
os.path.join(os.environ.get("LocalAppData", ""), "Programs", "Inkscape", "bin", "inkscape.exe")
])
elif platform.system() == "Darwin": # macOS
common_paths.extend([
"/Applications/Inkscape.app/Contents/MacOS/inkscape",
"/Applications/Inkscape.app/Contents/Resources/bin/inkscape"
])
else: # Linux
common_paths.extend([
"/usr/bin/inkscape",
"/usr/local/bin/inkscape",
"/snap/bin/inkscape",
"/opt/inkscape/bin/inkscape"
])
for path in common_paths:
if os.path.exists(path):
return path
return None
def log(self, message, level="info"):
"""Add a message to the log area with specified level (info, success, warning, error)"""
self.log_area.configure(state='normal')
self.log_area.insert(END, message + "\n", level)
self.log_area.configure(state='disabled')
self.log_area.see(END)
self.update_idletasks()
def clear_log(self):
"""Clear the log area"""
self.log_area.configure(state='normal')
self.log_area.delete(1.0, END)
self.log_area.configure(state='disabled')
def add_files(self):
"""Add files through file dialog"""
filetypes = [("SVG Files", "*.svg"), ("All Files", "*.*")]
files = filedialog.askopenfilenames(title="Select SVG Files", filetypes=filetypes)
if files:
self.update_file_list(files)
def add_folder(self):
"""Add all SVG files from a folder"""
folder = filedialog.askdirectory(title="Select Folder with SVG Files")
if folder:
svg_files = []
for root, _, files in os.walk(folder):
for file in files:
if file.lower().endswith('.svg'):
svg_files.append(os.path.join(root, file))
if svg_files:
self.update_file_list(svg_files)
else:
self.log("No SVG files found in the selected folder.", "warning")
def handle_drop(self, event):
"""Handle files dropped onto the window"""
files = self.tk.splitlist(event.data)
valid_files = []
for file in files:
# Handle both file paths and folder paths
if os.path.isdir(file):
for root, _, files_in_dir in os.walk(file):
for f in files_in_dir:
if f.lower().endswith('.svg'):
valid_files.append(os.path.join(root, f))
elif file.lower().endswith('.svg'):
valid_files.append(file)
if valid_files:
self.update_file_list(valid_files)
else:
self.log("No valid SVG files found in dropped items.", "warning")
def update_file_list(self, new_files):
"""Update the file list with new files, avoiding duplicates"""
existing_files = set(self.selected_files)
added = 0
for file in new_files:
file_path = os.path.abspath(file)
if file_path not in existing_files:
self.selected_files.append(file_path)
existing_files.add(file_path)
added += 1
if added > 0:
self.refresh_file_list()
self.log(f"Added {added} file(s) to conversion list.", "info")
else:
self.log("No new files were added (duplicates ignored).", "info")
def refresh_file_list(self):
"""Refresh the file list display"""
self.file_list.delete(0, END)
for file in self.selected_files:
self.file_list.insert(END, file)
def remove_files(self):
"""Remove selected files from the list"""
selected = self.file_list.curselection()
if selected:
# Remove in reverse order to maintain correct indices
for i in sorted(selected, reverse=True):
self.selected_files.pop(i)
self.refresh_file_list()
self.log(f"Removed {len(selected)} file(s) from conversion list.", "info")
else:
self.log("No files selected to remove.", "warning")
def clear_files(self):
"""Clear all files from the list"""
if self.selected_files:
self.selected_files.clear()
self.refresh_file_list()
self.log("Cleared all files from conversion list.", "info")
else:
self.log("File list is already empty.", "info")
def pick_color(self):
"""Open color picker dialog"""
color = filedialog.askcolor(title="Select Background Color", initialcolor=self.bg_var.get())
if color[1]: # color[1] is the hex string
self.bg_var.set(color[1])
def choose_output_dir(self):
"""Choose output directory"""
directory = filedialog.askdirectory(title="Select Output Directory")
if directory:
self.output_var.set(directory)
def start_conversion(self):
"""Start the conversion process in a separate thread"""
if not self.selected_files:
self.log("No files selected for conversion.", "warning")
return
if not self.inkscape_path:
self.log("Error: Inkscape not found. Please install Inkscape and ensure it's in your PATH.", "error")
return
if self.conversion_running:
self.log("Conversion is already in progress.", "warning")
return
self.conversion_running = True
self.failed_files = []
self.progress_var.set(0)
self.status_var.set("Converting...")
# Start conversion in a separate thread
threading.Thread(target=self.convert_files, daemon=True).start()
def convert_files(self):
"""Convert all selected files"""
total_files = len(self.selected_files)
success_count = 0
for i, input_file in enumerate(self.selected_files, 1):
if not self.conversion_running:
break
self.log(f"Processing file {i} of {total_files}: {input_file}")
self.status_var.set(f"Converting {i}/{total_files}: {os.path.basename(input_file)}")
try:
output_file = self.get_output_path(input_file)
if not self.overwrite_var.get() and os.path.exists(output_file):
self.log(f"Skipping (file exists): {output_file}", "warning")
continue
success = self.convert_file(input_file, output_file)
if success:
success_count += 1
self.log(f"Successfully converted: {output_file}", "success")
else:
self.failed_files.append(input_file)
self.log(f"Conversion failed: {input_file}", "error")
except Exception as e:
self.failed_files.append(input_file)
self.log(f"Error converting {input_file}: {str(e)}", "error")
# Update progress
self.progress_var.set((i / total_files) * 100)
self.update_idletasks()
# Conversion complete
self.conversion_running = False
self.status_var.set(f"Complete: {success_count} of {total_files} succeeded")
if success_count == total_files:
self.log(f"All {total_files} files converted successfully!", "success")
else:
self.log(f"Conversion complete with {total_files - success_count} failures.", "warning")
# Auto-open output folder if requested
if success_count > 0 and self.auto_open_var.get():
output_dir = self.get_output_dir(self.selected_files[0])
self.open_output_folder(output_dir)
def get_output_path(self, input_file):
"""Determine the output path for a given input file"""
input_path = Path(input_file)
output_dir = self.get_output_dir(input_file)
# Get format extension (default to png if not specified)
fmt = self.format_var.get().lower()
if fmt not in ['png', 'jpg', 'pdf', 'tiff']:
fmt = 'png'
output_filename = f"{input_path.stem}.{fmt}"
return str(output_dir / output_filename)
def get_output_dir(self, input_file):
"""Get the output directory for a given input file"""
output_dir = self.output_var.get()
if output_dir.lower() == 'same as source':
return Path(input_file).parent
else:
return Path(output_dir)
def convert_file(self, input_file, output_file):
"""Convert a single file using Inkscape"""
cmd = [
self.inkscape_path,
input_file,
"--export-type=" + self.format_var.get(),
"--export-filename=" + output_file,
f"--export-dpi={self.dpi_var.get()}",
f"--export-background={self.bg_var.get()}"
]
try:
# Run Inkscape and capture output
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
# Log any warnings from Inkscape
if result.stderr:
self.log(f"Inkscape output for {input_file}: {result.stderr}", "warning")
return os.path.exists(output_file)
except subprocess.CalledProcessError as e:
self.log(f"Inkscape error: {e.stderr}", "error")
return False
except Exception as e:
self.log(f"Unexpected error: {str(e)}", "error")
return False
def retry_failed(self):
"""Retry conversion of failed files"""
if not self.failed_files:
self.log("No failed files to retry.", "info")
return
self.selected_files = self.failed_files.copy()
self.failed_files = []
self.refresh_file_list()
self.start_conversion()
def open_output_folder(self, directory):
"""Open the output folder in the system file manager"""
try:
if platform.system() == "Windows":
os.startfile(directory)
elif platform.system() == "Darwin":
subprocess.run(["open", directory])
else: # Linux
subprocess.run(["xdg-open", directory])
except Exception as e:
self.log(f"Could not open output folder: {str(e)}", "warning")
def on_close(self):
"""Handle window close event"""
if self.conversion_running:
if messagebox.askokcancel(
"Conversion in Progress",
"A conversion is currently running. Are you sure you want to quit?"
):
self.conversion_running = False
self.destroy()
else:
self.save_settings()
self.destroy()
if __name__ == "__main__":
# Set up high DPI awareness on Windows
if platform.system() == "Windows":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
app = SVGtoPNGConverter()
app.mainloop()