gen-wallpapers: use DateTimeOriginal exif tag, module for location, preparation for the future

This commit is contained in:
Philippe Loctaux 2024-11-03 16:47:03 +01:00
parent a770e3e209
commit 74bcb1dcd0
2 changed files with 136 additions and 124 deletions

View file

@ -1,79 +1,27 @@
mod location;
use location::Gps;
use location::Location;
use location::parse_coordinates;
use exif::{DateTime, Exif, In, Tag};
use serde::Serialize;
use std::fs::ReadDir;
use std::io::BufReader;
fn parse_coordinates(exif: &Exif, tag: Tag, r#ref: Tag) -> Option<f32> {
let mut coord = None;
let mut coord_ref = None;
// Parse DMS coordinates
if let Some(field) = exif.get_field(tag, In::PRIMARY) {
match field.value {
exif::Value::Rational(ref vec) if !vec.is_empty() => {
let deg = vec[0].to_f64() as u16;
let min = vec[1].to_f64() as u8;
let sec = vec[2].to_f64();
coord = Some((deg, min, sec));
}
_ => {}
}
}
// Get bearing
if let Some(field) = exif.get_field(r#ref, In::PRIMARY) {
coord_ref = Some(field.display_value().to_string());
}
match (coord, coord_ref) {
(Some((deg, min, sec)), Some(r#ref)) => {
use dms_coordinates::Cardinal;
use dms_coordinates::Cardinal::*;
let bearing: Option<Cardinal> = match r#ref.as_str() {
"N" => Some(North),
"NE" => Some(NorthEast),
"NW" => Some(NorthWest),
"S" => Some(South),
"SE" => Some(SouthEast),
"SW" => Some(SouthWest),
"E" => Some(East),
"W" => Some(West),
_ => None,
};
let dms = dms_coordinates::DMS::new(deg, min, sec, bearing);
Some(dms.to_ddeg_angle() as f32)
}
(_, _) => None,
}
#[derive(Debug, Serialize)]
pub struct Metadata {
pub filename: String,
pub date: String,
pub width: u32,
pub height: u32,
pub gps: Gps,
pub location: Option<Location>,
}
#[derive(Debug, Serialize)]
struct Metadata {
filename: String,
date: String,
width: u32,
height: u32,
gps: Gps,
location: Option<Location>,
}
#[derive(Debug, Serialize)]
pub struct Location {
precise: String,
broad: String,
}
#[derive(Debug, Serialize)]
pub struct Gps {
latitude: f32,
longitude: f32,
}
#[derive(Debug, Serialize)]
pub struct MetadataList(Vec<Metadata>);
pub struct MetadataList(pub Vec<Metadata>);
impl MetadataList {
pub fn process_folder(dir: ReadDir, get_location: bool) -> Self {
@ -105,7 +53,7 @@ impl MetadataList {
// Get date
let mut date = None;
if let Some(field) = exif.get_field(Tag::DateTime, In::PRIMARY) {
if let Some(field) = exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
match field.value {
exif::Value::Ascii(ref vec) if !vec.is_empty() => {
if let Ok(datetime) = DateTime::from_ascii(&vec[0]) {
@ -118,6 +66,8 @@ impl MetadataList {
_ => {}
}
}
// TODO: get OffsetTimeOriginal as well
// TODO: use crate `jiff` to store dates with timezone offsets
// Get image width
let mut width = None;
@ -137,10 +87,7 @@ impl MetadataList {
match (date, latitude, longitude, width, height) {
(Some(date), Some(latitude), Some(longitude), Some(width), Some(height)) => {
let gps = Gps {
latitude,
longitude,
};
let gps = Gps::new(latitude,longitude);
let location = if get_location {
eprintln!("Getting location for `{}`", filename);
gps.get_location()
@ -151,7 +98,7 @@ impl MetadataList {
filename,
width,
height,
date,
date: date.to_string(),
gps,
location,
});
@ -169,54 +116,3 @@ impl MetadataList {
serde_json::to_string_pretty(&self.0)
}
}
impl Gps {
fn get_location(&self) -> Option<Location> {
match reqwest::blocking::Client::new()
.get(format!(
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={}&lon={}&zoom=8",
self.latitude, self.longitude,
))
.header("User-Agent", "https://philippeloctaux.com")
.send()
.and_then(|data| data.json::<serde_json::Value>())
{
Ok(data) => {
let location = &data["display_name"];
if location.is_string() {
let location = location.to_string();
// Remove first and last characters (the string is wrapped in double quotes '"')
let location = {
let mut chars = location.chars();
chars.next();
chars.next_back();
chars.as_str()
};
eprintln!("Raw location is `{}`", location);
let mut location = location.split(',');
let precise = location.next().unwrap_or("?").to_string();
let mut broad: String =
location.collect::<Vec<&str>>().join(",").trim().to_string();
if broad.is_empty() {
broad.push('?');
}
let location = Location { precise, broad };
eprintln!("Location is `{:?}`", location);
Some(location)
} else {
eprintln!("Failed to find location.");
None
}
}
Err(_) => {
eprintln!("Failed to make API call to get location.");
None
}
}
}
}

View file

@ -0,0 +1,116 @@
use exif::{Exif, In, Tag};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Location {
precise: String,
broad: String,
}
#[derive(Debug, Serialize)]
pub struct Gps {
latitude: f32,
longitude: f32,
}
impl Gps {
pub fn new(latitude: f32, longitude: f32) -> Self {
Self { latitude, longitude }
}
pub fn get_location(&self) -> Option<Location> {
match reqwest::blocking::Client::new()
.get(format!(
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={}&lon={}&zoom=8",
self.latitude, self.longitude,
))
.header("User-Agent", "https://philippeloctaux.com")
.send()
.and_then(|data| data.json::<serde_json::Value>())
{
Ok(data) => {
let location = &data["display_name"];
if location.is_string() {
let location = location.to_string();
// Remove first and last characters (the string is wrapped in double quotes '"')
let location = {
let mut chars = location.chars();
chars.next();
chars.next_back();
chars.as_str()
};
eprintln!("Raw location is `{}`", location);
let mut location = location.split(',');
let precise = location.next().unwrap_or("?").to_string();
let mut broad: String =
location.collect::<Vec<&str>>().join(",").trim().to_string();
if broad.is_empty() {
broad.push('?');
}
let location = Location { precise, broad };
eprintln!("Location is `{:?}`", location);
Some(location)
} else {
eprintln!("Failed to find location.");
None
}
}
Err(_) => {
eprintln!("Failed to make API call to get location.");
None
}
}
}
}
pub fn parse_coordinates(exif: &Exif, tag: Tag, r#ref: Tag) -> Option<f32> {
let mut coord = None;
let mut coord_ref = None;
// Parse DMS coordinates
if let Some(field) = exif.get_field(tag, In::PRIMARY) {
match field.value {
exif::Value::Rational(ref vec) if !vec.is_empty() => {
let deg = vec[0].to_f64() as u16;
let min = vec[1].to_f64() as u8;
let sec = vec[2].to_f64();
coord = Some((deg, min, sec));
}
_ => {}
}
}
// Get bearing
if let Some(field) = exif.get_field(r#ref, In::PRIMARY) {
coord_ref = Some(field.display_value().to_string());
}
match (coord, coord_ref) {
(Some((deg, min, sec)), Some(r#ref)) => {
use dms_coordinates::Cardinal;
use dms_coordinates::Cardinal::*;
let bearing: Option<Cardinal> = match r#ref.as_str() {
"N" => Some(North),
"NE" => Some(NorthEast),
"NW" => Some(NorthWest),
"S" => Some(South),
"SE" => Some(SouthEast),
"SW" => Some(SouthWest),
"E" => Some(East),
"W" => Some(West),
_ => None,
};
let dms = dms_coordinates::DMS::new(deg, min, sec, bearing);
Some(dms.to_ddeg_angle() as f32)
}
(_, _) => None,
}
}