Add GPS EXIF tags to timestamped photos by matching them to a FIT or GPX track. Track Time Tagger works locally: the CLI supports JPEG/TIFF, and the optional browser GUI produces geotagged JPEG copies without uploading photos or tracks.
The primary interface is a small interactive Rust CLI that:
- parses a Garmin/ANT
.fitfile withfitparseror a.gpxtrack withgpx, - reads
DateTimeOriginalfrom local JPEG/TIFF images, - interprets that timezone-less camera timestamp in a chosen IANA timezone,
- finds the surrounding GPS samples and linearly interpolates the position,
- prints OpenStreetMap and Google Maps links for verification,
- prompts before writing GPS EXIF metadata through its built-in Rust writer.
The built-in writer is used by default, so no Perl or ExifTool installation is required. It retains an <image>_original backup unless --no-backup is given. Existing GPS tags are skipped unless --overwrite-gps is given.
The default writer has no external runtime requirements. Build with Rust:
cargo build --releasecargo build --releaseThe binary will be at:
target/release/track-time-tagger
Generate completions for the shell you use and save the script where that shell loads completions:
# Bash
track-time-tagger --completions bash > ~/.local/share/bash-completion/completions/track-time-tagger
# Zsh
track-time-tagger --completions zsh > ~/.zfunc/_track-time-tagger
# Fish
track-time-tagger --completions fish > ~/.config/fish/completions/track-time-tagger.fishFor Zsh, add ~/.zfunc to fpath if it is not already included, then restart
the shell or reload its completion system.
GitHub Actions validates formatting, tests, Clippy, and an optimized CLI build
on every push and pull request. Pushing a version tag such as v0.1.0 creates
a GitHub release with native CLI archives for Linux x86_64, macOS ARM64, and
Windows x86_64.
The CLI remains the primary application. An optional Dioxus GUI lives in
gui/ as the foundation for a browser-first, local-only workflow.
It does not send selected photos or tracks to a server.
The GUI currently supports local JPEG processing: choose or drag in one FIT/GPX track and one or more JPEGs, review the local image previews, then download a GPS-tagged copy of each matching image. It is deliberately separate from the CLI, so normal CLI builds do not pull in GUI dependencies.
Install the Dioxus development tool and the WebAssembly target once:
cargo install dioxus-cli
rustup target add wasm32-unknown-unknownThen serve the GUI locally:
cd gui
dx serve --webFor a desktop preview instead, use dx serve --desktop. Platform setup notes
and the privacy model are in the GUI guide.
track-time-tagger \
--track activity.fit \
--images ./photos \
--timezone America/Toronto \
--recursive \
--dry-runEach match prints optional map-provider links for verifying its proposed location.
Suppose a professional photographer covers a road race with a camera whose clock is accurate, but whose photos contain no GPS metadata. A runner or other subject can carry a Garmin watch or another GPS device during the event, then use the original FIT file or a GPX export to retrofit GPS metadata after receiving the photographer's images.
The camera provides the timestamp for each photo, while the track file provides the position for each moment. The tool combines those two sources, interpolating between nearby track points when necessary. This can place photos taken at aid stations, turns, finish lines, and along the course on a map without requiring a GPS-enabled camera.
First preview the proposed locations without changing any photos:
cargo run --release -- \
--track race-day.fit \
--images ./race-photos \
--timezone America/New_York \
--recursive \
--dry-runIf the preview matches the event route, run the update interactively:
cargo run --release -- \
--track race-day.fit \
--images ./race-photos \
--timezone America/New_York \
--recursiveIf the camera clock was consistently 12 seconds behind the GPS device, include --camera-offset-seconds 12. Existing GPS metadata is skipped by default, so the workflow is safe to repeat while reviewing the results.
GPX input works the same way:
cargo run --release -- \
--track race-day.gpx \
--images ./race-photos \
--timezone America/New_York \
--recursive \
--dry-runThe track format is selected from the .fit or .gpx filename extension. GPX track points without timestamps are ignored.
A hiker or birdwatcher may carry a professional SLR camera with excellent image quality but no built-in GPS. As long as the camera clock is accurate, the Android OpenTracks app on Google Play or F-Droid can record the outing independently. Export that activity as GPX, then use Track Time Tagger to add the corresponding GPS position to each photo afterward.
This is useful when the camera and tracking device are separate: the camera records the best image, while the phone or watch records the route. Accurate timestamps provide the link between them.
track-time-tagger \
--track activity.fit \
--images ./photos \
--timezone America/Toronto \
--recursiveThe program asks separately for each photo before writing.
By default, accepted matches update the input images in place. Use --output to leave the originals unchanged. When scanning a directory, the input directory layout is reproduced below the output directory:
track-time-tagger \
--track hike.gpx \
--images ./camera-originals \
--output ./camera-geotagged \
--recursive \
--yesFor a single input image, --output is treated as the destination directory for that image. Backups, when enabled, are created beside the copied output image.
If the camera was 37 seconds slow, add 37 seconds before matching:
--camera-offset-seconds 37If the camera was two minutes fast, subtract 120 seconds:
--camera-offset-seconds -120- Images already containing GPS tags are skipped by default.
--overwrite-gpspermits replacing them.--max-gap-seconds 300requires both surrounding track points to be no more than five minutes away.--dry-runnever prompts or writes.--yesaccepts all valid matches.--output DIRwrites updated copies underDIRand leaves input images unchanged.--no-backupdisables the<image>_originalbackup for either metadata writer. Leave it off initially.- Supported input images are JPEG and TIFF.
Standard EXIF DateTimeOriginal usually has no timezone. FIT and GPX timestamps represent an absolute time. Therefore --timezone must describe the timezone in which the camera clock was set when the photos were taken.
During an ambiguous fall daylight-saving transition, the tool refuses the timestamp rather than silently choosing the wrong UTC instant.
The standard installation uses only the integrated Rust metadata writer. For the optional ExifTool backend, see the advanced ExifTool guide.