feat(v2.3.0): add JSON flattening, fix file handle leaks, update README
This commit is contained in:
parent
5d838bc27e
commit
c34e2d663c
3 changed files with 106 additions and 19 deletions
94
README.md
94
README.md
|
|
@ -1,16 +1,34 @@
|
||||||
# PNG Metadata Editor v1.0.0
|
# PNG Metadata Editor v2.3.0
|
||||||
|
|
||||||
A graphical tool for viewing and editing metadata in PNG files.
|
A graphical tool for viewing and editing metadata in PNG files with an intuitive image browser interface.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
### Core Features
|
||||||
- View all metadata fields (tEXt, zTXt, iTXt chunks) in PNG files
|
- View all metadata fields (tEXt, zTXt, iTXt chunks) in PNG files
|
||||||
- Add, edit, and delete metadata fields
|
- Add, edit, and delete metadata fields
|
||||||
- Pretty-print JSON-formatted values
|
|
||||||
- Copy metadata values to clipboard
|
- Copy metadata values to clipboard
|
||||||
- Visual indication of unsaved changes
|
- Visual indication of unsaved changes
|
||||||
|
- Pretty-print JSON values in editor (auto-flattens on save)
|
||||||
|
|
||||||
|
### Image Browser
|
||||||
|
- Browse directories with thumbnail previews
|
||||||
|
- Auto-scroll to selected images
|
||||||
|
- Trackpad and mousewheel scrolling support
|
||||||
|
- Smooth navigation between multiple files
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
- Automatic dark mode detection (macOS, Windows, Linux)
|
||||||
|
- Adaptive canvas background based on system theme
|
||||||
|
- Unicode support throughout UI
|
||||||
|
- Resizable panes for optimal workflow
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
- Smart JSON handling: displays formatted, saves flattened
|
||||||
|
- Proper file handle management for reliable multi-file loading
|
||||||
|
- Enhanced scroll region updates for smooth browsing
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -24,21 +42,37 @@ A graphical tool for viewing and editing metadata in PNG files.
|
||||||
Install required packages using pip:
|
Install required packages using pip:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install pillow pngmeta
|
pip install pillow
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Basic Workflow
|
||||||
|
|
||||||
1. Run the application:
|
1. Run the application:
|
||||||
```bash
|
```bash
|
||||||
python png-meta-editor.py
|
python png-meta-editor.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Open a PNG file using the "Open PNG File" button
|
2. **Browse Directory**: Click "Browse Directory" to load thumbnails from a folder
|
||||||
|
3. **Select Image**: Click any thumbnail to load its metadata
|
||||||
|
4. **View Metadata**: See all fields in the tree view with preview
|
||||||
|
5. **Edit Fields**:
|
||||||
|
- Double-click or use "Edit Field" to modify values
|
||||||
|
- JSON values display formatted but save flattened
|
||||||
|
6. **Save Changes**: Click "Save Changes" to write metadata back to file
|
||||||
|
|
||||||
3. View metadata in the tree view on the left
|
### Keyboard Shortcuts
|
||||||
4. Select a field to see its full value in the detail pane
|
|
||||||
5. Use the buttons to add, edit, or delete fields
|
- **Double-click** on tree item to edit
|
||||||
|
- **Enter/Escape** in dialogs to confirm/cancel
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
The application automatically detects your system theme:
|
||||||
|
- **macOS**: Reads `AppleInterfaceStyle` setting
|
||||||
|
- **Windows**: Reads personalization registry settings
|
||||||
|
- **Linux**: Checks GTK theme preferences
|
||||||
|
|
||||||
## Building for Distribution
|
## Building for Distribution
|
||||||
|
|
||||||
|
|
@ -68,7 +102,7 @@ pyinstaller --name="PNG Metadata Editor" \
|
||||||
png-meta-editor.py
|
png-meta-editor.py
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mac OS
|
#### macOS
|
||||||
```bash
|
```bash
|
||||||
pyinstaller --name="PNG Metadata Editor" \
|
pyinstaller --name="PNG Metadata Editor" \
|
||||||
--windowed \
|
--windowed \
|
||||||
|
|
@ -94,15 +128,53 @@ For building, you'll need:
|
||||||
- `AppIcon.icns` (Mac icon)
|
- `AppIcon.icns` (Mac icon)
|
||||||
- `requirements.txt` (for dependencies)
|
- `requirements.txt` (for dependencies)
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### JSON Handling
|
||||||
|
|
||||||
|
The editor intelligently handles JSON metadata:
|
||||||
|
- **Display**: Pretty-printed with indentation for readability
|
||||||
|
- **Storage**: Automatically flattened to single line to preserve original format
|
||||||
|
- **Example**:
|
||||||
|
```json
|
||||||
|
// What you see in editor:
|
||||||
|
{
|
||||||
|
"prompt": "a cat",
|
||||||
|
"steps": 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// What gets saved:
|
||||||
|
{"prompt":"a cat","steps":20}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Management
|
||||||
|
|
||||||
|
- Proper file handle release after reading metadata
|
||||||
|
- Supports loading multiple files sequentially without issues
|
||||||
|
- Thumbnail caching for smooth browsing experience
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see [LICENSE](LICENSE) for details.
|
This project is licensed under the MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
Robert Tusa
|
Robert Tusa
|
||||||
robert@tusa.at
|
robert@tusa.at
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
- **1.0.0 (2026-01-05)**: Initial release
|
### v2.3.0 (2026-01-07) - Major UI Update
|
||||||
|
- Added image browser with thumbnail previews
|
||||||
|
- Implemented dark mode detection for all platforms
|
||||||
|
- Added trackpad/mousewheel scrolling support
|
||||||
|
- Smart JSON flattening (preserves original format)
|
||||||
|
- Fixed file loading issues with proper handle management
|
||||||
|
- Enhanced scroll region updates
|
||||||
|
- Unicode support in UI elements
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-05) - Initial Release
|
||||||
|
- Basic metadata viewing and editing
|
||||||
|
- Tree view with detail pane
|
||||||
|
- Add, edit, delete operations
|
||||||
|
- JSON pretty-printing
|
||||||
|
|
@ -94,10 +94,10 @@ class PNGMetadataEditor:
|
||||||
top_frame = ttk.Frame(self.root, padding="10")
|
top_frame = ttk.Frame(self.root, padding="10")
|
||||||
top_frame.pack(fill=tk.X)
|
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="🌌 Open PNG File", command=self.open_file).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(top_frame, text="📥 Browse Directory", command=self.browse_directory).pack(side=tk.LEFT, padx=5)
|
ttk.Button(top_frame, text="🗂️ Browse Directory", command=self.browse_directory).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(top_frame, text="✅ Save Changes", command=self.save_changes).pack(side=tk.LEFT, padx=5)
|
ttk.Button(top_frame, text="💾 Save Changes", command=self.save_changes).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(top_frame, text="ℹ️ About", command=self.show_about).pack(side=tk.RIGHT, padx=5)
|
ttk.Button(top_frame, text="🍺 About", command=self.show_about).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
self.file_label = ttk.Label(top_frame, text="No file loaded", foreground="gray")
|
self.file_label = ttk.Label(top_frame, text="No file loaded", foreground="gray")
|
||||||
self.file_label.pack(side=tk.LEFT, padx=20)
|
self.file_label.pack(side=tk.LEFT, padx=20)
|
||||||
|
|
@ -191,10 +191,10 @@ class PNGMetadataEditor:
|
||||||
button_frame = ttk.Frame(editor_frame, padding="10")
|
button_frame = ttk.Frame(editor_frame, padding="10")
|
||||||
button_frame.pack(fill=tk.X)
|
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="✍️ 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="✏️ 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="❌ 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)
|
ttk.Button(button_frame, text="📋 Copy Value", command=self.copy_value).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
# Status bar at the bottom
|
# Status bar at the bottom
|
||||||
status_frame = ttk.Frame(self.root, relief=tk.SUNKEN)
|
status_frame = ttk.Frame(self.root, relief=tk.SUNKEN)
|
||||||
|
|
@ -421,6 +421,8 @@ class PNGMetadataEditor:
|
||||||
if hasattr(img, 'text'):
|
if hasattr(img, 'text'):
|
||||||
self.metadata_dict = dict(img.text)
|
self.metadata_dict = dict(img.text)
|
||||||
|
|
||||||
|
img.close() # Release file handle
|
||||||
|
|
||||||
self.file_label.config(text=Path(filepath).name, foreground="")
|
self.file_label.config(text=Path(filepath).name, foreground="")
|
||||||
self.mark_as_saved()
|
self.mark_as_saved()
|
||||||
self.refresh_tree()
|
self.refresh_tree()
|
||||||
|
|
@ -494,6 +496,15 @@ class PNGMetadataEditor:
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def flatten_json_if_valid(self, value):
|
||||||
|
"""If value is valid JSON, return it flattened (no indent), otherwise return as-is"""
|
||||||
|
try:
|
||||||
|
parsed = json.loads(value)
|
||||||
|
return json.dumps(parsed, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def truncate_value(self, value, max_length=80):
|
def truncate_value(self, value, max_length=80):
|
||||||
"""Truncate long values for tree display"""
|
"""Truncate long values for tree display"""
|
||||||
value_str = str(value)
|
value_str = str(value)
|
||||||
|
|
@ -583,7 +594,9 @@ class PNGMetadataEditor:
|
||||||
messagebox.showwarning("Invalid Input", "Key cannot be empty")
|
messagebox.showwarning("Invalid Input", "Key cannot be empty")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.metadata_dict[key] = value
|
# Flatten JSON if valid to keep original format
|
||||||
|
flattened_value = self.flatten_json_if_valid(value)
|
||||||
|
self.metadata_dict[key] = flattened_value
|
||||||
self.mark_as_modified()
|
self.mark_as_modified()
|
||||||
self.refresh_tree()
|
self.refresh_tree()
|
||||||
self.set_status(f"Added field '{key}'", color="green")
|
self.set_status(f"Added field '{key}'", color="green")
|
||||||
|
|
@ -637,7 +650,9 @@ class PNGMetadataEditor:
|
||||||
|
|
||||||
def save_edit():
|
def save_edit():
|
||||||
new_value = value_text.get(1.0, tk.END).strip()
|
new_value = value_text.get(1.0, tk.END).strip()
|
||||||
self.metadata_dict[key] = new_value
|
# Flatten JSON if valid to keep original format
|
||||||
|
flattened_value = self.flatten_json_if_valid(new_value)
|
||||||
|
self.metadata_dict[key] = flattened_value
|
||||||
self.mark_as_modified()
|
self.mark_as_modified()
|
||||||
self.refresh_tree()
|
self.refresh_tree()
|
||||||
self.set_status(f"Updated field '{key}'", color="green")
|
self.set_status(f"Updated field '{key}'", color="green")
|
||||||
|
|
|
||||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 1.8 MiB |
Loading…
Add table
Add a link
Reference in a new issue