rsimagetag - Photo Tagging GUI Tool
A graphical application for tagging and organizing photos, written in Rust.
Features
- Browse photos in a native GUI built with egui
- Tag people using Google Contacts
resourceNamereferences — no ambiguity with duplicate names - Tag scenes with free-form labels (e.g., “beach”, “wedding”, “birthday”)
- Content-based identification using SHA-256 image hashes — tags survive file renames and moves
- Persistent ACID database powered by redb at
~/.config/rsimagetag/tags.redb - Import people from Google Contacts via rscontacts
- Search by tag to find all photos of a specific person or scene
How It Works
rsimagetag computes a SHA-256 hash of each image’s content and stores tags against that hash. Tags are plain strings — if a tag starts with people/c it references a Google Contact, otherwise it is a free-form tag (scene, event, location, etc.).
- Renaming a file does not lose its tags.
- Moving a file to a different folder does not lose its tags.
- Duplicate copies of the same image share the same tags.
Technology
- Built with Rust using eframe/egui for the GUI
- redb embedded database for ACID-safe tag storage
- SHA-256 (sha2) for content-based image identification
- clap for CLI
- Integration with rscontacts for people import
Installation
Prerequisites
- Rust toolchain (edition 2024)
Install from crates.io
cargo install rsimagetag
This downloads, compiles, and installs the latest published version into ~/.cargo/bin/.
Building from Source
git clone https://github.com/veltzer/rsimagetag.git
cd rsimagetag
cargo build --release
The binary will be at target/release/rsimagetag.
Install Directly
cargo install --path .
Desktop Integration
rsimagetag provides a built-in command to install itself as a proper desktop application
with icon and .desktop file support for KDE, GNOME, and other freedesktop-compliant
desktop environments.
Installing the Desktop Entry
After building, run:
rsimagetag install-desktop
This installs:
- Icon files at
~/.local/share/icons/hicolor/{64x64,128x128,256x256}/apps/rsimagetag.png - Desktop entry at
~/.local/share/applications/rsimagetag.desktop - Icon theme index — copies
/usr/share/icons/hicolor/index.themeinto~/.local/share/icons/hicolor/if it is missing
After installation, you may need to update your icon cache:
gtk-update-icon-cache ~/.local/share/icons/hicolor/
kbuildsycoca6 # KDE only
How the Icon Works
rsimagetag generates its application icon programmatically at runtime — there are no static icon image files in the repository. The icon is a white “T” on a teal (#009688) rounded-rectangle background.
- Window icon: Generated via
generate_icon()and passed to eframe’sViewportBuilder::with_icon()at startup. This sets the icon shown in the window title bar. - Taskbar/launcher icon: Generated as PNG files by
install-desktopand installed into the hicolor icon theme at multiple sizes (64, 128, 256 pixels). The.desktopfile references these viaIcon=rsimagetag.
Troubleshooting
Icon not showing in taskbar or application launcher
This is typically caused by one or more of the following issues:
Missing icon theme index
The ~/.local/share/icons/hicolor/ directory must contain an index.theme file for
the icon cache to be built correctly. Without it, gtk-update-icon-cache fails and
desktop environments cannot discover installed icons.
Run install-desktop again — it now copies the system index.theme automatically
if it is missing. Alternatively, copy it manually:
cp /usr/share/icons/hicolor/index.theme ~/.local/share/icons/hicolor/index.theme
Stale icon cache
After installing or updating icons, the GTK icon cache and KDE’s sycoca database must be refreshed:
gtk-update-icon-cache ~/.local/share/icons/hicolor/
kbuildsycoca6 # KDE Plasma
If the icon still does not appear, try logging out and back in.
Icon size too small
Some desktop environments (especially KDE Plasma) request icons at sizes larger than 64x64 for the taskbar and application launcher. If only a 64x64 icon is available, the desktop environment may fall back to a generic icon instead of upscaling.
The install-desktop command installs icons at 64x64, 128x128, and 256x256 to cover
all common sizes.
StartupWMClass mismatch
The .desktop file must have a StartupWMClass that matches the window’s
resourceClass as reported by the desktop environment. For rsimagetag, both are set
to rsimagetag. The app_id passed to eframe’s ViewportBuilder::with_app_id()
controls this value on Wayland.
You can verify the window class using KDE’s window properties (right-click title bar and select “More Actions” > “Configure Special Window Settings”) or by running:
kdotool search --class rsimagetag
Wayland vs X11
On Wayland, the application ID (set via with_app_id("rsimagetag")) is used to match
the window to its .desktop file. On X11, the WM_CLASS property is used instead.
Ensure the app_id / WM_CLASS matches the StartupWMClass in the .desktop file.
Getting Started
First-Time Setup
After building rsimagetag, initialize the tag database:
rsimagetag db-init
This creates ~/.config/rsimagetag/tags.redb with the tags and people lookup tables. You only need to do this once.
Importing People from Google Contacts
Import your contacts so you can tag people in photos by name:
rsimagetag db-import-rscontacts
This requires rscontacts to be installed and authenticated. The import populates the people lookup table with resourceName → display_name entries.
Launching the Application
Browse and tag images in the current directory:
rsimagetag tag
Or specify a directory:
rsimagetag tag --dir ~/Pictures
Basic Workflow
- Navigate through images using the Prev/Next buttons or keyboard shortcuts (Arrow keys, N/P).
- Add tags to each image — select people from the imported contacts list, or type scene tags.
- Tags are automatically saved to the database.
How Tags Work
Tags are plain strings stored in a flat list per image:
["people/c1234567890", "people/c9876543210", "beach", "sunset"]
- Tags starting with
people/creference a Google Contact — the UI shows the display name from the people lookup table. - All other tags are free-form (scenes, events, locations, etc.).
Inspecting the Database
Dump the full database as JSON:
rsimagetag db-dump
Output:
{
"people": {
"people/c1234567890": "Alice Smith",
"people/c9876543210": "Bob Jones"
},
"image_tags": {
"a1b2c3...": ["people/c1234567890", "beach"],
"d4e5f6...": ["people/c9876543210", "sunset"]
}
}
Commands
rsimagetag db-init # Initialize the database (first time only)
rsimagetag db-dump # Dump the entire database as JSON
rsimagetag tag # Browse and tag images in current directory
rsimagetag tag --dir ~/Pictures # Browse and tag images in a specific directory
rsimagetag version # Print version and build info
rsimagetag complete bash # Generate shell completions
Concepts
Image Hashing
rsimagetag identifies images by their content hash rather than their file path. When you open an image, the application computes a SHA-256 hash of the file data. This hash serves as the unique identifier for that image in the database.
Benefits:
- Rename-proof: Moving or renaming a file does not affect its tags.
- Deduplication: Multiple copies of the same image automatically share tags.
- Portable: The database is meaningful even if your folder structure changes.
Tags
Tags are plain strings stored in a flat list per image. The type of a tag is determined by convention:
People Tags
A tag that starts with people/c is a reference to a Google Contacts resourceName (e.g., people/c1234567890). This is the stable unique identifier that Google assigns to each contact.
Benefits of using resourceName instead of a display name:
- No ambiguity: Two contacts named “Mike” have different resourceNames.
- Rename-safe: If someone changes their name in Google Contacts, the tag still points to the right person — only the display name in the people lookup table needs updating.
- Scalable: Works with 10 or 10,000 contacts.
Scene / Free-Form Tags
Any tag that does not start with people/c is a free-form tag. Use these for scenes, events, locations, dates, or anything else. Examples: beach, wedding, birthday, vacation, 2024.
Tag Detection
The application determines the tag type with a simple prefix check:
if tag.starts_with("people/c") → person reference → look up display name
else → free-form tag → display as-is
This convention is extensible — future tag types can use new prefixes (e.g., location/..., event/...) without schema changes.
Database
The database is an embedded redb store located at ~/.config/rsimagetag/tags.redb. It contains two tables:
Tags Table
Maps image content hashes to tag lists.
| Key | Value |
|---|---|
| SHA-256 hex (64 chars) | JSON array of tag strings |
Example:
["people/c1234567890", "people/c9876543210", "beach", "sunset"]
People Lookup Table
Maps Google Contacts resourceName to display name. This is a lookup-only table used by the UI to show human-readable names instead of opaque IDs.
| Key | Value |
|---|---|
people/c1234567890 | Alice Smith |
people/c9876543210 | Bob Jones |
This table is populated by importing contacts from rscontacts.
Initialization
The database must be initialized before first use:
rsimagetag db-init
This creates the ~/.config/rsimagetag/ directory and the tags.redb database file with both tables.
The database is ACID-compliant — tags are never lost due to crashes or power failures.
Design Decisions
GUI Framework: egui via eframe
Approach Chosen: egui/eframe
rsimagetag uses egui as its GUI framework, running via the eframe integration.
Alternatives Considered
- gtk4-rs: Native Linux look and feel, but adds a system dependency on GTK4 libraries.
- Qt via cxx-qt: Mature and feature-rich, but the Rust bindings add significant complexity.
- iced: Elm-inspired architecture, but less mature ecosystem than egui.
- Tauri: Web-based UI, but adds a web stack dependency for a desktop app.
Why egui
- Pure Rust: No system dependencies beyond OpenGL/Vulkan — builds anywhere Rust builds.
- Simple API: Immediate mode GUI is easy to prototype and iterate on.
- Good image support: egui handles image rendering well, which is critical for a photo app.
- Active community: Well-maintained with frequent releases and good documentation.
Image Identification: Content Hashing
Approach Chosen: SHA-256 Hash of File Content
Each image is identified by a SHA-256 hash of its file content. Tags are stored against this hash in the database.
Alternative Considered: File Path
Using the file path as the identifier would be simpler but fragile — renaming or moving a file would lose all its tags.
Why Content Hashing
- Robust: Tags survive file renames, moves, and reorganization.
- Deduplication: Duplicate copies of the same image automatically share tags.
- Trade-off: Slightly slower on first load (must read and hash each file), but hashes can be cached.
Tag Database: redb
Approach Chosen: redb Embedded Database
rsimagetag uses redb as its embedded key-value store, located at ~/.config/rsimagetag/tags.redb.
Alternatives Considered
- sled: Pure Rust embedded database, but maintenance has been inconsistent and its future is uncertain.
- SQLite via rusqlite: Battle-tested relational database, but heavier than needed for a simple key-value mapping. Adds a C dependency.
- serde_json flat file: Simplest possible approach, but no ACID guarantees — a crash during write could corrupt the entire database.
Why redb
- Pure Rust: No system dependencies or C libraries to link.
- ACID-compliant: Crash-safe — tags are never lost due to power failures or unexpected termination.
- Simple API: Perfect for key-value mappings.
- Consistent: Already used by rscontacts, keeping the tooling consistent across projects.
- Lightweight: Minimal overhead compared to SQLite for our use case.
Tag Schema: Flat String List with Prefix Convention
Approach Chosen: Tags as Plain Strings
Each image’s tags are stored as a flat Vec<String>. The type of tag is determined by prefix convention:
people/c...→ person reference (Google ContactsresourceName)- anything else → free-form tag (scene, event, location, etc.)
Alternative Considered: Structured Tags with Separate Fields
An earlier design used a struct with separate people: Vec<String> and scenes: Vec<String> fields per image. This was rejected in favor of the flat list.
Why Flat Strings
- Simpler code: One
add_tag/remove_tagfunction instead of two of everything. - More flexible: Adding a new tag type (e.g.,
location/...,event/...) requires zero schema changes — just use a new prefix. - Simpler JSON:
["people/c123", "beach"]instead of{"people": [...], "scenes": [...]}. - No performance difference: A typical image has 5-20 tags. The
starts_with("people/c")check is negligible. - The people lookup table handles display: The people table (
resourceName → display_name) is a separate lookup table, not part of the tag schema.
People Identification: Google Contacts resourceName
Approach Chosen: Reference by resourceName
People in photos are identified by their Google Contacts resourceName (e.g., people/c1234567890), not by display name.
Alternative Considered: Display Name Strings
Using display names like "Alice Smith" would be simpler but creates problems with duplicate names and name changes.
Why resourceName
- Unique: Google guarantees each contact has a unique
resourceName. Two contacts named “Mike” have different resourceNames. - Stable: The
resourceNamedoes not change when a contact’s name is updated. If “Bob Smith” changes to “Robert Smith”, all photo tags still reference the same person. - Scalable: Works equally well with 10 contacts or 10,000.
- Integration: Importing from rscontacts (which uses the Google People API) naturally provides resourceNames.
The display name is stored separately in the people lookup table and resolved at display time.
Future Ideas
This page collects ideas for future development of rsimagetag.
Face Detection
Integrate face detection to automatically suggest people tags. When a photo is opened, the application could detect faces and either auto-tag known people or prompt the user to identify unknown faces. Libraries like OpenCV (via the opencv crate) or ONNX-based models could provide this functionality.
Additional Tag Prefixes
The flat string tag system is designed for extensibility via prefixes. Future tag types could include:
location/...— geographic locations (e.g.,location/paris,location/home)event/...— named events (e.g.,event/wedding-2024,event/birthday-bob)date/...— date tags (e.g.,date/2024-07)
No schema changes would be needed — just new prefix conventions and UI support.
Bulk Import
Add support for scanning an entire directory tree and presenting untagged images for batch tagging. This would help with initial setup when importing an existing photo collection.
Export and Sync
Support exporting the tag database in standard formats (JSON, CSV) and syncing it across machines. The db-dump command already provides JSON export; a corresponding db-import command would enable restoring from backups or syncing between devices.
Smart Albums
Automatically generate albums based on tag combinations. For example, “All beach photos with Alice” or “All 2024 birthday photos”. These albums would update dynamically as new photos are tagged.
Thumbnail Cache
Generate and cache thumbnails for faster browsing of large photo collections. The thumbnails would be stored alongside the tag database and regenerated as needed.
rscontacts JSON Export
Add a --json flag to rscontacts’ list command to output contacts as structured JSON with resourceName and display name fields. This would make the import into rsimagetag more robust than parsing the current text output.