Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 resourceName references — 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.theme into ~/.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’s ViewportBuilder::with_icon() at startup. This sets the icon shown in the window title bar.
  • Taskbar/launcher icon: Generated as PNG files by install-desktop and installed into the hicolor icon theme at multiple sizes (64, 128, 256 pixels). The .desktop file references these via Icon=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

  1. Navigate through images using the Prev/Next buttons or keyboard shortcuts (Arrow keys, N/P).
  2. Add tags to each image — select people from the imported contacts list, or type scene tags.
  3. 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/c reference 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.

KeyValue
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.

KeyValue
people/c1234567890Alice Smith
people/c9876543210Bob 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 Contacts resourceName)
  • 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_tag function 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 resourceName does 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.