Geotagging is easy. Rust CLI app
3 min read January 26, 2025 #openlayers #rust #maps #cliIn this section, we will create a CLI application in Rust that collects all the necessary data to display photos on the map: coordinates, some camera information, a thumbnail, and will also generate the map.
By specifying a directory path, the application will recursively go through the photos, collect GPS coordinates where possible, collect photo details, create previews, and generate a JSON file that will then be embedded into an HTML map template.
As you might have guessed, all the information, including the thumbnails, will be taken from the Exif data. Here is the output from the exiftool program, a small part of what Exif contains:
File Type JPEG
Exif Byte Order Little-endian
Make Sony
Camera Model Name Xperia Z1
Exposure Time 1/500
F Number 2.0
ISO 50
Exif Version 0220
Date/Time Original 2021:05:28 14:56:43
Create Date 2021:05:28 14:56:43
Shutter Speed Value 1/498
Aperture Value 2.0
Focal Length 4.9 mm
Color Space sRGB
Exif Image Width 3554
Exif Image Height 2000
GPS Version ID 2.0.0.0
GPS Map Datum WGS-84
Compression JPEG
GPS Latitude 47 deg 45GPS Longitude 37 deg 16GPS Latitude Ref North
GPS Longitude Ref East
Thumbnail Offset 1006
Thumbnail Length 6483
The most important thing here is the GPS coordinates. Also we can get the camera manufacturer and model, and the date the photo was taken. Exif can also contain a preview, usually with a width of 160 pixels, which will be sufficient for us.
Creating the project
Create a new project with the --bin
flag, indicating that this will be an application, not a library:
Dependencies:
[]
= "0.6.1"
= "2.5"
= { = "1.0", = ["derive"] }
= "1.0"
serde
for JSON serialization, walkdir
for recursive directory traversal, kamadak-exif
for working with Exif.
exifgeo
Read the directory path from the command line arguments:
The zeroth argument contains the path to the executable program, so skip this argument by specifying .nth(1)
.
Exif is supported not only by JPG files but also by many other formats, so it would be better to scan them too:
const SUPPORTED_EXTENSIONS: = ;
Recursively traverse the directory using WalkDir, considering only files (e.file_type().is_file()
) with supported extensions (SUPPORTED_EXTENSIONS
):
let photos = new
.into_iter
.filter_map
.filter
.filter
.enumerate
.filter_map
.filter
.;
Next, read the Exif data and collect the necessary information. If the data is present, fill the PhotoInfo
structure with photo information, and then select only those with non-zero longitude and latitude.
Be sure to specify #[derive(Serialize)]
so that serde can serialize it.
Finally, read Exif and fill the PhotoInfo
structure:
let exif = match open ;
let name = e.file_name.to_string_lossy.to_string;
let path = e.path;
let latitude = get_coord?;
let longitude = get_coord?;
let make = get_string.unwrap_or;
let model = get_string.unwrap_or;
let date = get_datetime.unwrap_or;
let thumb = get_thumbnail_data
.and_then?;
Some
At the beginning of the article, I provided an example of what Exif data looks like. Here we simply read certain tags, not forgetting to check the tag data types in the specification. If the Exif thumbnail exists, we read and save it under a sequential number in a separate thumbs
folder.
I won't go into the Exif details, I'll write about it in the next article.
So, at this stage, we have a vector with photo data. We serialize it into a JSON string:
let map_json = to_string.unwrap_or;
Prepare the HTML template from the previous article and include it into the program binary using the include_bytes!
macro:
static MAP_HTML: & = include_bytes!;
In the HTML template, where the JSON with geodata should be loaded, insert a special marker:
handleMapDataLoaded;
All that's left is to embed the obtained JSON into the HTML page and write it to a file:
let html = String from_utf8_lossy.replace;
let mut file = create.expect;
file.write_all.expect;
We embed it by simply replacing the string {map:1}
with the obtained JSON.
Building the program
You can run the program using cargo:
Or by building the binary:
However, this will build a debug version. To build a minimized release version, you should add to Cargo.toml:
[]
= true
= "z"
= true
and execute:
Run the program:
An HTML file with a map is generated:
I also built the program for Android using Termux:
Everything works there as well.
📄 Source code
🔽 Download app