Upload files to "/"
This commit is contained in:
parent
3bf5af7c5e
commit
cd371b15db
2 changed files with 390 additions and 0 deletions
390
png-meta-editor.py
Normal file
390
png-meta-editor.py
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
from pathlib import Path
|
||||
import json
|
||||
from PIL import Image
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
class PNGMetadataEditor:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("PNG Metadata Editor")
|
||||
self.root.geometry("900x700")
|
||||
|
||||
self.current_file = None
|
||||
self.metadata_dict = {}
|
||||
self.has_unsaved_changes = False
|
||||
self.status_timer = None
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# Bring window to foreground on startup
|
||||
self.root.lift()
|
||||
self.root.attributes('-topmost', True)
|
||||
self.root.after_idle(self.root.attributes, '-topmost', False)
|
||||
self.root.focus_force()
|
||||
|
||||
def setup_ui(self):
|
||||
# Top frame for file operations
|
||||
top_frame = ttk.Frame(self.root, padding="10")
|
||||
top_frame.pack(fill=tk.X)
|
||||
|
||||
ttk.Button(top_frame, text="Open PNG File", command=self.open_file).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(top_frame, text="Save Changes", command=self.save_changes).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.file_label = ttk.Label(top_frame, text="No file loaded", foreground="gray")
|
||||
self.file_label.pack(side=tk.LEFT, padx=20)
|
||||
|
||||
# Unsaved changes indicator
|
||||
self.changes_label = ttk.Label(top_frame, text="", foreground="orange")
|
||||
self.changes_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# PanedWindow for splitting tree and detail view
|
||||
paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
|
||||
paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# Top pane - Treeview
|
||||
tree_frame = ttk.Frame(paned)
|
||||
paned.add(tree_frame, weight=2)
|
||||
|
||||
# Treeview with scrollbar
|
||||
tree_scroll = ttk.Scrollbar(tree_frame)
|
||||
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.tree = ttk.Treeview(tree_frame, columns=("Key", "Value"), show="headings",
|
||||
yscrollcommand=tree_scroll.set)
|
||||
self.tree.heading("Key", text="Key")
|
||||
self.tree.heading("Value", text="Value Preview")
|
||||
self.tree.column("Key", width=200)
|
||||
self.tree.column("Value", width=600)
|
||||
self.tree.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
tree_scroll.config(command=self.tree.yview)
|
||||
|
||||
# Bind selection to update detail view
|
||||
self.tree.bind("<<TreeviewSelect>>", self.on_selection_change)
|
||||
# Bind double-click to edit
|
||||
self.tree.bind("<Double-1>", self.edit_entry)
|
||||
|
||||
# Bottom pane - Detail view
|
||||
detail_frame = ttk.Frame(paned)
|
||||
paned.add(detail_frame, weight=1)
|
||||
|
||||
ttk.Label(detail_frame, text="Value Detail:", font=("TkDefaultFont", 10, "bold")).pack(anchor=tk.W, padx=5, pady=5)
|
||||
|
||||
# Text widget with scrollbar for value display
|
||||
detail_scroll = ttk.Scrollbar(detail_frame)
|
||||
detail_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.detail_text = tk.Text(detail_frame, wrap=tk.WORD, yscrollcommand=detail_scroll.set,
|
||||
font=("Menlo", 11), height=8)
|
||||
self.detail_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
detail_scroll.config(command=self.detail_text.yview)
|
||||
|
||||
# Button frame for operations
|
||||
button_frame = ttk.Frame(self.root, padding="10")
|
||||
button_frame.pack(fill=tk.X)
|
||||
|
||||
ttk.Button(button_frame, text="Add Field", command=self.add_field).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Edit Field", command=self.edit_entry).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Delete Field", command=self.delete_field).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Copy Value", command=self.copy_value).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Status bar at the bottom
|
||||
status_frame = ttk.Frame(self.root, relief=tk.SUNKEN)
|
||||
status_frame.pack(fill=tk.X, side=tk.BOTTOM)
|
||||
|
||||
self.status_label = ttk.Label(status_frame, text="Ready", padding=(10, 5))
|
||||
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
|
||||
def set_status(self, message, duration=3000, color=""):
|
||||
"""Set status bar message that auto-clears after duration (ms)"""
|
||||
# Cancel any existing timer
|
||||
if self.status_timer:
|
||||
self.root.after_cancel(self.status_timer)
|
||||
|
||||
# Set the message
|
||||
self.status_label.config(text=message, foreground=color)
|
||||
|
||||
# Schedule clearing the message
|
||||
if duration > 0:
|
||||
self.status_timer = self.root.after(duration, lambda: self.status_label.config(text="Ready", foreground=""))
|
||||
|
||||
def mark_as_modified(self):
|
||||
"""Mark the document as having unsaved changes"""
|
||||
if not self.has_unsaved_changes:
|
||||
self.has_unsaved_changes = True
|
||||
self.update_title()
|
||||
self.changes_label.config(text="Unsaved changes")
|
||||
|
||||
def mark_as_saved(self):
|
||||
"""Mark the document as saved"""
|
||||
self.has_unsaved_changes = False
|
||||
self.update_title()
|
||||
self.changes_label.config(text="")
|
||||
|
||||
def update_title(self):
|
||||
"""Update window title with unsaved indicator"""
|
||||
base_title = "PNG Metadata Editor"
|
||||
if self.current_file:
|
||||
filename = Path(self.current_file).name
|
||||
if self.has_unsaved_changes:
|
||||
self.root.title(f"{base_title} - {filename} *")
|
||||
else:
|
||||
self.root.title(f"{base_title} - {filename}")
|
||||
else:
|
||||
self.root.title(base_title)
|
||||
|
||||
def format_value_for_display(self, value):
|
||||
"""Try to format value as JSON if possible, otherwise return as-is"""
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
parsed = json.loads(value)
|
||||
# If successful, return pretty-printed JSON
|
||||
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# Not JSON, return original value
|
||||
return value
|
||||
|
||||
def truncate_value(self, value, max_length=80):
|
||||
"""Truncate long values for tree display"""
|
||||
value_str = str(value)
|
||||
if len(value_str) > max_length:
|
||||
return value_str[:max_length] + "..."
|
||||
return value_str
|
||||
|
||||
def on_selection_change(self, event):
|
||||
"""Update detail view when selection changes"""
|
||||
selected = self.tree.selection()
|
||||
if selected:
|
||||
item = selected[0]
|
||||
key, _ = self.tree.item(item, "values")
|
||||
|
||||
if key in self.metadata_dict:
|
||||
value = self.metadata_dict[key]
|
||||
formatted_value = self.format_value_for_display(value)
|
||||
|
||||
# Update detail text
|
||||
self.detail_text.config(state=tk.NORMAL)
|
||||
self.detail_text.delete(1.0, tk.END)
|
||||
self.detail_text.insert(1.0, formatted_value)
|
||||
self.detail_text.config(state=tk.DISABLED)
|
||||
|
||||
def open_file(self):
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="Select PNG file",
|
||||
filetypes=[("PNG files", "*.png"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
if filepath:
|
||||
try:
|
||||
self.current_file = filepath
|
||||
|
||||
# Read metadata using PIL
|
||||
img = Image.open(filepath)
|
||||
self.metadata_dict = {}
|
||||
|
||||
# Extract text chunks (tEXt, zTXt, iTXt)
|
||||
if hasattr(img, 'text'):
|
||||
self.metadata_dict = dict(img.text)
|
||||
|
||||
self.file_label.config(text=Path(filepath).name, foreground="")
|
||||
self.mark_as_saved()
|
||||
self.refresh_tree()
|
||||
|
||||
field_count = len(self.metadata_dict)
|
||||
self.set_status(f"Loaded {field_count} metadata field(s) from {Path(filepath).name}", color="green")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to open file:\n{str(e)}")
|
||||
self.set_status("Failed to open file", color="red")
|
||||
|
||||
def refresh_tree(self):
|
||||
# Clear existing items
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
|
||||
# Clear detail view
|
||||
self.detail_text.config(state=tk.NORMAL)
|
||||
self.detail_text.delete(1.0, tk.END)
|
||||
self.detail_text.config(state=tk.DISABLED)
|
||||
|
||||
# Add metadata entries from dictionary
|
||||
for key, value in self.metadata_dict.items():
|
||||
truncated = self.truncate_value(value)
|
||||
self.tree.insert("", tk.END, values=(key, truncated))
|
||||
|
||||
def add_field(self):
|
||||
if self.current_file is None:
|
||||
messagebox.showwarning("No File", "Please open a PNG file first")
|
||||
return
|
||||
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Add Field")
|
||||
dialog.geometry("600x400")
|
||||
dialog.minsize(400, 300)
|
||||
|
||||
# Make dialog resizable
|
||||
dialog.rowconfigure(1, weight=1)
|
||||
dialog.columnconfigure(1, weight=1)
|
||||
|
||||
# Key entry
|
||||
ttk.Label(dialog, text="Key:").grid(row=0, column=0, padx=10, pady=10, sticky=tk.W)
|
||||
key_entry = ttk.Entry(dialog, width=50)
|
||||
key_entry.grid(row=0, column=1, padx=10, pady=10, sticky=tk.EW)
|
||||
|
||||
# Value text with label
|
||||
ttk.Label(dialog, text="Value:").grid(row=1, column=0, padx=10, pady=10, sticky=tk.NW)
|
||||
|
||||
# Frame for text widget and scrollbar
|
||||
text_frame = ttk.Frame(dialog)
|
||||
text_frame.grid(row=1, column=1, padx=10, pady=10, sticky=tk.NSEW)
|
||||
|
||||
value_scroll = ttk.Scrollbar(text_frame)
|
||||
value_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
value_text = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=value_scroll.set,
|
||||
font=("Menlo", 11))
|
||||
value_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
value_scroll.config(command=value_text.yview)
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(dialog)
|
||||
button_frame.grid(row=2, column=0, columnspan=2, pady=10, sticky=tk.E)
|
||||
|
||||
def save_new_field():
|
||||
key = key_entry.get().strip()
|
||||
value = value_text.get(1.0, tk.END).strip()
|
||||
|
||||
if not key:
|
||||
messagebox.showwarning("Invalid Input", "Key cannot be empty")
|
||||
return
|
||||
|
||||
self.metadata_dict[key] = value
|
||||
self.mark_as_modified()
|
||||
self.refresh_tree()
|
||||
self.set_status(f"Added field '{key}'", color="green")
|
||||
dialog.destroy()
|
||||
|
||||
ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="Add", command=save_new_field).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
key_entry.focus()
|
||||
|
||||
def edit_entry(self, event=None):
|
||||
selected = self.tree.selection()
|
||||
if not selected:
|
||||
messagebox.showwarning("No Selection", "Please select a field to edit")
|
||||
return
|
||||
|
||||
item = selected[0]
|
||||
key, _ = self.tree.item(item, "values")
|
||||
current_value = self.metadata_dict[key]
|
||||
|
||||
# Format value as JSON if possible
|
||||
formatted_value = self.format_value_for_display(current_value)
|
||||
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Edit Field")
|
||||
dialog.geometry("600x400")
|
||||
dialog.minsize(400, 300)
|
||||
|
||||
# Make dialog resizable
|
||||
dialog.rowconfigure(1, weight=1)
|
||||
dialog.columnconfigure(1, weight=1)
|
||||
|
||||
# Key display
|
||||
ttk.Label(dialog, text="Key:").grid(row=0, column=0, padx=10, pady=10, sticky=tk.W)
|
||||
key_label = ttk.Label(dialog, text=key, font=("TkDefaultFont", 10, "bold"))
|
||||
key_label.grid(row=0, column=1, padx=10, pady=10, sticky=tk.W)
|
||||
|
||||
# Value text with label
|
||||
ttk.Label(dialog, text="Value:").grid(row=1, column=0, padx=10, pady=10, sticky=tk.NW)
|
||||
|
||||
# Frame for text widget and scrollbar
|
||||
text_frame = ttk.Frame(dialog)
|
||||
text_frame.grid(row=1, column=1, padx=10, pady=10, sticky=tk.NSEW)
|
||||
|
||||
value_scroll = ttk.Scrollbar(text_frame)
|
||||
value_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
value_text = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=value_scroll.set,
|
||||
font=("Menlo", 11))
|
||||
value_text.insert(1.0, formatted_value)
|
||||
value_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
value_scroll.config(command=value_text.yview)
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(dialog)
|
||||
button_frame.grid(row=2, column=0, columnspan=2, pady=10, sticky=tk.E)
|
||||
|
||||
def save_edit():
|
||||
new_value = value_text.get(1.0, tk.END).strip()
|
||||
self.metadata_dict[key] = new_value
|
||||
self.mark_as_modified()
|
||||
self.refresh_tree()
|
||||
self.set_status(f"Updated field '{key}'", color="green")
|
||||
dialog.destroy()
|
||||
|
||||
ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="Save", command=save_edit).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
value_text.focus()
|
||||
|
||||
def delete_field(self):
|
||||
selected = self.tree.selection()
|
||||
if not selected:
|
||||
messagebox.showwarning("No Selection", "Please select a field to delete")
|
||||
return
|
||||
|
||||
item = selected[0]
|
||||
key, _ = self.tree.item(item, "values")
|
||||
|
||||
if messagebox.askyesno("Confirm Delete", f"Delete field '{key}'?"):
|
||||
if key in self.metadata_dict:
|
||||
del self.metadata_dict[key]
|
||||
self.mark_as_modified()
|
||||
self.refresh_tree()
|
||||
self.set_status(f"Deleted field '{key}'", color="green")
|
||||
|
||||
def copy_value(self):
|
||||
selected = self.tree.selection()
|
||||
if not selected:
|
||||
messagebox.showwarning("No Selection", "Please select a field to copy")
|
||||
return
|
||||
|
||||
item = selected[0]
|
||||
key, _ = self.tree.item(item, "values")
|
||||
value = self.metadata_dict[key]
|
||||
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(value)
|
||||
self.set_status(f"Copied value of '{key}' to clipboard", color="green")
|
||||
|
||||
def save_changes(self):
|
||||
if self.current_file is None:
|
||||
messagebox.showwarning("No File", "Please open a PNG file first")
|
||||
return
|
||||
|
||||
try:
|
||||
# Open the image
|
||||
img = Image.open(self.current_file)
|
||||
|
||||
# Create new PngInfo object with only our metadata
|
||||
metadata = PngInfo()
|
||||
for key, value in self.metadata_dict.items():
|
||||
metadata.add_text(key, value)
|
||||
|
||||
# Save the image with new metadata (this replaces all text chunks)
|
||||
img.save(self.current_file, pnginfo=metadata)
|
||||
|
||||
self.mark_as_saved()
|
||||
self.set_status("Metadata saved successfully", color="green")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to save metadata:\n{str(e)}")
|
||||
self.set_status("Failed to save metadata", color="red")
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = PNGMetadataEditor(root)
|
||||
root.mainloop()
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 KiB |
Loading…
Add table
Add a link
Reference in a new issue