├── .rustfmt.toml ├── src ├── core │ ├── mod.rs │ ├── app.rs │ └── cli.rs ├── util │ ├── mod.rs │ ├── file.rs │ └── location.rs ├── lib.rs ├── main.rs ├── exif_writer │ ├── mod.rs │ └── exiftool.rs └── location_reader │ ├── mod.rs │ └── life_path.rs ├── .gitignore ├── Cargo.toml ├── .github └── workflows │ └── release.yml ├── LICENSE ├── README.md ├── README_en.md └── Cargo.lock /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces=2 -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod cli; 3 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod location; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | /tmp 4 | nya-exif.log 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod core; 2 | pub mod util; 3 | pub mod exif_writer; 4 | pub mod location_reader; 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use nya_exif::core::cli; 2 | 3 | extern crate simple_log; 4 | 5 | fn main() { 6 | cli::run(); 7 | } 8 | -------------------------------------------------------------------------------- /src/exif_writer/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub mod exiftool; 4 | 5 | pub struct ExifWriterParam { 6 | pub binary_path: Option, 7 | } 8 | 9 | pub trait ExifWriterBase { 10 | fn read_timestamp(&mut self, path: &str) -> i64; 11 | fn write_location(&mut self, path: &str, lat: f64, lon: f64, alt: f64); 12 | fn change_timezone_offset(&mut self, path: &str, timezone_offset: &str); 13 | fn close(&mut self) -> std::io::Result<()>; 14 | } 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nya-exif" 3 | version = "0.1.3" 4 | edition = "2021" 5 | authors = ["Lyn "] 6 | description = "A cross-platform tool for embedding GPS data into photographs" 7 | repository = "https://github.com/LynMoe/nya-exif" 8 | readme = "README.md" 9 | 10 | [dependencies] 11 | chrono = "0.4.31" 12 | clap = { version = "4.4.7", features = ["derive"] } 13 | csv = "1.3.0" 14 | dirs = "5.0.1" 15 | indicatif = "0.17.7" 16 | os_info = "3.7.0" 17 | simple-log = "1.6.0" 18 | undrift_gps = "0.3.1" 19 | which = "5.0.0" 20 | -------------------------------------------------------------------------------- /src/location_reader/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | pub mod life_path; 3 | 4 | pub struct LocationReaderParam { 5 | pub data_path: Option, 6 | pub time_offset: i32, 7 | pub max_interval: u32, 8 | } 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct LocationReaderResult { 12 | pub lat: f64, 13 | pub lon: f64, 14 | pub alt: f64, 15 | pub confidence_radius: f32, 16 | pub timestamp: i32, 17 | } 18 | 19 | pub trait LocationReaderBase { 20 | // fn new(param: ExifWriterParam) -> Self; 21 | fn get_location(&mut self, timestamp: i32) -> Option; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | name: release ${{ matrix.target }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - target: x86_64-pc-windows-gnu 16 | archive: zip 17 | - target: x86_64-unknown-linux-musl 18 | archive: tar.gz 19 | - target: x86_64-apple-darwin 20 | archive: tar.gz 21 | steps: 22 | - uses: actions/checkout@master 23 | - name: Compile and release 24 | uses: rust-build/rust-build.action@v1.4.4 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | RUSTTARGET: ${{ matrix.target }} 29 | ARCHIVE_TYPES: ${{ matrix.archive }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lyn Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/util/file.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | use simple_log::log::debug; 6 | 7 | pub fn read_dir_files(dir: &Path, full_path: bool, recursive: bool) -> io::Result> { 8 | let exts = vec!["jpg", "jpeg", "png", "cr3", "dng", "heic", "RW2", "raw", "raf", "arw", "arq", "nef", "nrw"]; 9 | 10 | debug!("[Util readDir] Reading dir: {:?}", dir); 11 | 12 | let mut files = Vec::new(); 13 | 14 | if dir.is_dir() { 15 | for entry in fs::read_dir(dir)? { 16 | let entry = entry?; 17 | let path = entry.path(); 18 | 19 | if path.is_dir() && recursive { 20 | files.extend(read_dir_files(&path, recursive, full_path)?); 21 | } else if path.is_file() { 22 | if path.file_name().unwrap().to_string_lossy().starts_with(".") { 23 | continue; 24 | } 25 | let ext = path.extension().unwrap_or("".to_owned().as_ref()).to_string_lossy().to_lowercase(); 26 | if !exts.contains(&ext.as_ref()) { 27 | debug!("[Util readDir] Skipping file: {:?}", path); 28 | continue; 29 | } 30 | 31 | files.push(if full_path { 32 | path.to_string_lossy().to_string() 33 | } else { 34 | path.file_name().unwrap().to_string_lossy().to_string() 35 | }); 36 | } 37 | } 38 | } 39 | 40 | Ok(files) 41 | } 42 | -------------------------------------------------------------------------------- /src/exif_writer/exiftool.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::io::{BufRead, BufReader}; 3 | use std::path::PathBuf; 4 | use std::process::{Command, ChildStdin, ChildStdout, Stdio, Child}; 5 | use chrono::DateTime; 6 | use simple_log::log::debug; 7 | use which::which; 8 | 9 | use crate::exif_writer::{ExifWriterBase, ExifWriterParam}; 10 | 11 | pub struct ExifWriterExifTool { 12 | child: Child, 13 | stdin: ChildStdin, 14 | stdout: BufReader, 15 | } 16 | 17 | impl ExifWriterExifTool { 18 | pub fn new(param: ExifWriterParam) -> Self { 19 | let path: PathBuf; 20 | 21 | if param.binary_path.is_none() { 22 | // auto detect 23 | path = match os_info::get().os_type() { 24 | _ => { 25 | which("exiftool").unwrap() 26 | } 27 | }; 28 | } else { 29 | path = PathBuf::from(param.binary_path.unwrap()); 30 | } 31 | 32 | let mut child = Command::new(&path) 33 | .arg("-stay_open") 34 | .arg("true") 35 | .arg("-@") 36 | .arg("-") 37 | .stdin(Stdio::piped()) 38 | .stdout(Stdio::piped()) 39 | .spawn() 40 | .expect("Failed to start exiftool"); 41 | 42 | let stdin = child.stdin.take().unwrap(); 43 | let stdout = BufReader::new(child.stdout.take().unwrap()); 44 | 45 | Self { child, stdin, stdout } 46 | } 47 | 48 | fn execute_command(&mut self, filename: &str, args: &[String]) -> String { 49 | writeln!(self.stdin, "{}", filename).unwrap(); 50 | for arg in args { 51 | writeln!(self.stdin, "{}", arg).unwrap(); 52 | } 53 | writeln!(self.stdin, "-execute").unwrap(); 54 | 55 | let mut result = String::new(); 56 | loop { 57 | let mut line = String::new(); 58 | self.stdout.read_line(&mut line).unwrap(); 59 | if line.trim() == "{ready}" { 60 | break; 61 | } 62 | result.push_str(&line); 63 | } 64 | 65 | debug!("[Exiftool] arg: {:?}, result: {}", args, result); 66 | 67 | result 68 | } 69 | } 70 | 71 | impl ExifWriterBase for ExifWriterExifTool { 72 | fn read_timestamp(&mut self, path: &str) -> i64 { 73 | let args = [ 74 | "-createDate".to_string(), 75 | "-d".to_string(), 76 | "%Y:%m:%d %H:%M:%S %z".to_string(), 77 | ]; 78 | 79 | // let output = 80 | let result = self.execute_command(path, &args); 81 | let lines: Vec<&str> = result.split("\n").map(|line| { 82 | let mut ll = line.split(": "); 83 | 84 | if line.len() == 0 { 85 | return ""; 86 | } 87 | 88 | ll.next(); 89 | ll.next().unwrap().trim() 90 | }).collect(); 91 | 92 | let time_string = lines[0].to_owned(); 93 | let dt = DateTime::parse_from_str(&time_string, "%Y:%m:%d %H:%M:%S %z") 94 | .expect("Failed to parse date time string"); 95 | 96 | debug!("[Exiftool] time_string: {}, parse timestamp: {}", time_string, dt.timestamp()); 97 | 98 | dt.timestamp() 99 | } 100 | 101 | fn write_location(&mut self, path: &str, lat: f64, lon: f64, alt: f64) { 102 | let args = [ 103 | "-GPSLatitude=".to_string() + &lat.to_string(), 104 | "-GPSLongitude=".to_string() + &lon.to_string(), 105 | "-GPSLatitudeRef=N".to_string(), 106 | "-GPSLongitudeRef=E".to_string(), 107 | "-GPSAltitude=".to_string() + &alt.to_string(), 108 | "-GPSAltitudeRef=0".to_string(), 109 | "-overwrite_original".to_string(), 110 | ]; 111 | 112 | // let output = 113 | self.execute_command(path, &args); 114 | } 115 | 116 | fn change_timezone_offset(&mut self, path: &str, timezone_offset: &str) { 117 | let args = [ 118 | "-EXIF:OffsetTime*=".to_string() + timezone_offset, // eg, +2:00 119 | "-overwrite_original".to_string(), 120 | ]; 121 | 122 | // let output = 123 | self.execute_command(path, &args); 124 | } 125 | 126 | fn close(&mut self) -> std::io::Result<()> { 127 | self.child.kill()?; 128 | 129 | self.child.wait()?; 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/util/location.rs: -------------------------------------------------------------------------------- 1 | use simple_log::log::debug; 2 | 3 | // Approximate GCJ region in (latitude, longitude) pairs 4 | static GCJ_REGION: &'static [(f64, f64)] = &[ 5 | (52.731367549831, 120.18298955774142), 6 | (53.63032135052725, 122.50163662235207), 7 | (53.26176144514083, 125.77607688660949), 8 | (49.793642660678515, 127.87680506936519), 9 | (49.407718349606306, 129.66811795485958), 10 | (47.998560826930955, 131.530512732968), 11 | (48.51048270954458, 134.84037096447878), 12 | (45.12562855907086, 133.32895840353396), 13 | (44.79216287150945, 131.28848995673235), 14 | (42.63680250697947, 130.45592176815663), 15 | (42.99527458867438, 129.91790514760166), 16 | (42.04761801712594, 128.0687300505346), 17 | (41.45987408769909, 128.10876970001843), 18 | (41.780407957507286, 126.81985564688428), 19 | (38.461741231751844, 121.50306067363671), 20 | (37.414082923391454, 122.93061875172674), 21 | (30.674023244049664, 123.07426160685057), 22 | (24.6033327288128, 119.22457427978614), 23 | (21.179240961699527, 112.43694704179752), 24 | (17.673075974060612, 109.48709815312112), 25 | (19.138508820560002, 108.18083521881559), 26 | (21.39327464004528, 108.15083770831288), 27 | (22.080063041679676, 106.60666332280034), 28 | (22.71632309622958, 106.5008040544447), 29 | (23.13617888669703, 105.0537266093335), 30 | (22.32102047283515, 101.78903194655028), 31 | (21.091729915864516, 101.81916659472037), 32 | (21.497462709277563, 100.27712620789795), 33 | (22.172525330672745, 99.21924485723063), 34 | (23.922710480065003, 97.59174752286235), 35 | (24.7766324211775, 97.52779222878526), 36 | (25.979741935612175, 98.59304557492919), 37 | (27.518813240263356, 98.41488901761588), 38 | (29.255978242960396, 96.05290813407619), 39 | (28.98351539963584, 94.3743597614614), 40 | (27.73202645570489, 91.91576503922244), 41 | (27.954988751231465, 89.43637256086063), 42 | (27.305474672237125, 88.87924298495538), 43 | (27.926305488252357, 85.9660573850956), 44 | (30.018093251412424, 81.1821861261154), 45 | (31.29101575822806, 78.8959522774237), 46 | (32.51080027519665, 78.40327588060015), 47 | (32.70102288420409, 79.42731032370726), 48 | (34.73972040936371, 78.19756139986893), 49 | (35.46498638604698, 78.08744745345531), 50 | (35.827127091240776, 76.13888392627935), 51 | (36.12722522700187, 75.92489090497999), 52 | (36.74619160023912, 75.56385329098687), 53 | (37.09152700706339, 74.49469911660775), 54 | (37.36092398871107, 75.07085973927778), 55 | (38.52410020540065, 73.96571879042757), 56 | (39.45869189286305, 73.55981403057588), 57 | (40.48588468250351, 74.8404885479942), 58 | (41.4079098041435, 78.18892031992831), 59 | (42.209549490371245, 80.1685385590239), 60 | (43.0232779792319, 80.3924877595656), 61 | (44.92499622938564, 79.91450165082203), 62 | (45.27242675378137, 82.50522457891103), 63 | (47.22146706317084, 82.95321261550617), 64 | (47.06392532516057, 85.49226819096441), 65 | (48.43342505864067, 85.73634505323675), 66 | (49.146033236672466, 86.88083279984656), 67 | (49.157812554992795, 87.77826911573229), 68 | (47.862960480730074, 90.01921884921369), 69 | (46.02248151184977, 91.03156364579425), 70 | (45.21092618689381, 90.84795164769706), 71 | (44.33814457729536, 95.3781833450865), 72 | (42.77559669230801, 96.46889449961684), 73 | (42.50247036887638, 101.82589397754059), 74 | (41.939925797616525, 105.58392770472803), 75 | (42.757160841682044, 110.1889382502376), 76 | (43.753981039668, 111.87813257735903), 77 | (45.081602850466446, 111.76814565537664), 78 | (46.755407823982075, 119.02228883676571), 79 | (47.248791064053286, 119.54348219864653), 80 | (48.022727706580255, 117.82995057487629), 81 | (47.630522239677866, 117.36286596647822), 82 | (47.91831453430476, 115.56839450099663), 83 | (49.83589917843746, 116.6729235427556), 84 | (50.3781951866589, 119.13757348710827), 85 | (52.11976005858193, 120.72835099286257), 86 | (52.5816732367262, 120.15157047622971) 87 | ]; 88 | 89 | pub fn is_point_in_gcj_region(point: (f64, f64)) -> bool { 90 | let mut count = 0; 91 | let (px, py) = point; 92 | let mut j = GCJ_REGION.len() - 1; 93 | for i in 0..GCJ_REGION.len() { 94 | let (sx, sy) = GCJ_REGION[i]; 95 | let (ex, ey) = GCJ_REGION[j]; 96 | if (sy < py && ey >= py) || (ey < py && sy >= py) { 97 | if sx + (py - sy) / (ey - sy) * (ex - sx) < px { 98 | count += 1; 99 | } 100 | } 101 | j = i; 102 | } 103 | 104 | debug!("[Util Loca inGcj] Pair {:?}, count {}, in region: {}", point, count, count % 2 != 0); 105 | count % 2 != 0 106 | } 107 | -------------------------------------------------------------------------------- /src/location_reader/life_path.rs: -------------------------------------------------------------------------------- 1 | use csv::ReaderBuilder; 2 | use simple_log::log::{error, debug}; 3 | use std::fs::File; 4 | use std::io::{BufReader, Seek, SeekFrom}; 5 | use std::str; 6 | use std::env; 7 | 8 | use crate::location_reader::{LocationReaderBase, LocationReaderParam, LocationReaderResult}; 9 | 10 | pub struct LocationReaderLiftPath { 11 | param: LocationReaderParam, 12 | file: File, 13 | index: Vec<(i32, u64)>, // (timestamp, position) 14 | } 15 | 16 | impl LocationReaderLiftPath { 17 | pub fn new(param: LocationReaderParam) -> Self { 18 | let mut file_path; 19 | 20 | if env::consts::OS != "macos" && param.data_path.is_none() { 21 | error!("Location data file path(-f) required"); 22 | panic!(); 23 | } 24 | 25 | if param.data_path.is_none() { 26 | file_path = dirs::home_dir().unwrap(); 27 | file_path.push("Library/Mobile Documents/iCloud~com~lifubing~lbs~stepOfLife/Documents"); 28 | } else { 29 | file_path = param.data_path.clone().unwrap(); 30 | } 31 | file_path.push("backUpData.csv"); 32 | 33 | debug!("[LifePath] folder path: {:?}", file_path); 34 | 35 | if !file_path.exists() { 36 | error!("[LifePath] CSV file not exists, path {:?}", file_path); 37 | panic!(); 38 | } 39 | 40 | let file = File::open(&file_path).unwrap(); 41 | let mut reader = BufReader::new(file.try_clone().unwrap()); 42 | let mut index = Vec::new(); 43 | 44 | let mut rdr = ReaderBuilder::new() 45 | .has_headers(true) 46 | .from_reader(&mut reader); 47 | 48 | for result in rdr.byte_records() { 49 | let record = result.unwrap(); 50 | let pos = record.position().unwrap().byte(); 51 | 52 | let timestamp = str::from_utf8(&record[0]).unwrap().parse::().unwrap(); 53 | index.push((timestamp, pos)); 54 | } 55 | 56 | index.sort_by(|a: &(i32, u64), b| a.0.cmp(&b.0)); 57 | 58 | LocationReaderLiftPath { param, file, index } 59 | } 60 | } 61 | 62 | impl LocationReaderBase for LocationReaderLiftPath { 63 | fn get_location(&mut self, timestamp: i32) -> Option { 64 | let timestamp = timestamp - self.param.time_offset; 65 | let pos = self.find_closest_position(timestamp); 66 | if pos.is_none() { 67 | return None; 68 | } 69 | let pos = pos.unwrap(); 70 | 71 | self.file.seek(SeekFrom::Start(pos.0)).unwrap(); 72 | let mut rdr = ReaderBuilder::new() 73 | .has_headers(false) 74 | .from_reader(&self.file); 75 | let record1 = rdr.records().next()?.unwrap(); 76 | 77 | self.file.seek(SeekFrom::Start(pos.1)).unwrap(); 78 | let mut rdr = ReaderBuilder::new() 79 | .has_headers(false) 80 | .from_reader(&self.file); 81 | let record2 = rdr.records().next()?.unwrap(); 82 | 83 | debug!("[LifePath] search record1: {:?}", record1); 84 | debug!("[LifePath] search record2: {:?}", record2); 85 | 86 | let d1 = (record1[0].parse::().unwrap() - timestamp).abs(); 87 | let d2 = (record2[0].parse::().unwrap() - timestamp).abs(); 88 | let p1 = (d2 as f64) / (d1 as f64 + d2 as f64); 89 | let p2 = (d1 as f64) / (d1 as f64 + d2 as f64); 90 | 91 | let time_mid = ((record1[0].parse::().unwrap() as f64 * p1 + record2[0].parse::().unwrap() as f64 * p2)) as i32; 92 | let lat_mid = record1[3].parse::().unwrap() * p1 + record2[3].parse::().unwrap() * p2; 93 | let lon_mid = record1[2].parse::().unwrap() * p1 + record2[2].parse::().unwrap() * p2; 94 | let alt_mid = record1[10].parse::().unwrap() * p1 + record2[10].parse::().unwrap() * p2; 95 | let confidence_radius_min = record1[5].parse::().unwrap().min(record2[5].parse::().unwrap()); 96 | 97 | debug!("[LifePath] time_mid: {}, max interval: {}", time_mid, self.param.max_interval); 98 | 99 | if (time_mid - timestamp).abs() > self.param.max_interval as i32 { 100 | return None; 101 | } 102 | 103 | Some(LocationReaderResult { 104 | lat: lat_mid, 105 | lon: lon_mid, 106 | alt: alt_mid, 107 | confidence_radius: confidence_radius_min, 108 | timestamp: time_mid, 109 | }) 110 | } 111 | } 112 | 113 | impl LocationReaderLiftPath { 114 | fn find_closest_position(&self, timestamp: i32) -> Option<(u64, u64)> { 115 | if self.index.len() <= 2 { 116 | return None; 117 | } 118 | 119 | let mut left = 0; 120 | let mut right = self.index.len() - 1; 121 | 122 | while left < right { 123 | let mid = left + (right - left) / 2; 124 | if self.index[mid].0 < timestamp { 125 | left = mid + 1; 126 | } else { 127 | right = mid; 128 | } 129 | } 130 | 131 | if left == 0 { 132 | return Some((self.index[0].1, self.index[1].1)); 133 | } 134 | 135 | let diff2 = (timestamp - self.index[left - 1].0).abs(); 136 | let mut result = (left, left - 1); 137 | 138 | if left + 1 < self.index.len() { 139 | let diff3 = self.index[left + 1].0 - timestamp; 140 | if diff3 > diff2 { 141 | result = (left, left + 1); 142 | } 143 | } 144 | 145 | Some((self.index[result.0].1, self.index[result.1].1)) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/core/app.rs: -------------------------------------------------------------------------------- 1 | use crate::exif_writer::{exiftool::ExifWriterExifTool, ExifWriterBase, ExifWriterParam}; 2 | use crate::location_reader::{life_path, LocationReaderBase, LocationReaderParam}; 3 | use crate::util::{file, location as lc}; 4 | use clap::ValueEnum; 5 | use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; 6 | use simple_log::log::{info, warn}; 7 | use std::fmt::Write; 8 | use std::path::{Path, PathBuf}; 9 | use std::thread; 10 | use undrift_gps::wgs_to_gcj; 11 | 12 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 13 | pub enum ExifWriterType { 14 | /// ExitTool(https://exiftool.org/) 15 | Exiftool, 16 | } 17 | 18 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 19 | pub enum LocationReaderType { 20 | /// LifePath(一生足迹) 21 | LifePath, 22 | } 23 | 24 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 25 | pub enum LocationGpsCoordinateTarget { 26 | /// Global coordinate system 27 | WGS84, 28 | /// China coordinate system 29 | GCJ02, 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub struct AppParams { 34 | pub operate_dir: Vec, 35 | pub recursive: bool, 36 | pub thread_count: u32, 37 | pub writer_type: ExifWriterType, 38 | pub writer_bin_path: Option, 39 | pub location_reader_type: LocationReaderType, 40 | pub location_file_path: Option, 41 | pub location_max_interval: u32, 42 | pub location_gps_coordinate_target: Option, 43 | pub overwrite_original: bool, 44 | pub time_offset: i32, 45 | } 46 | 47 | pub fn run(params: AppParams) { 48 | let mut fi = vec![]; 49 | for dir in params.operate_dir.clone() { 50 | let fi_ = file::read_dir_files(dir.as_ref(), true, true).unwrap(); 51 | fi.extend(fi_); 52 | } 53 | 54 | // split fi in params.thread_count parts 55 | let mut fi_parts = vec![]; 56 | let mut now_state = 0; 57 | let mut now_part = 0; 58 | let part_size = fi.len() / params.thread_count as usize; 59 | for file in fi { 60 | if now_state == 0 { 61 | fi_parts.push(vec![]); 62 | } 63 | 64 | fi_parts[now_part].push(file); 65 | 66 | now_state += 1; 67 | if now_state == part_size && now_part + 1 < params.thread_count as usize { 68 | now_state = 0; 69 | now_part += 1; 70 | } 71 | } 72 | 73 | let m = MultiProgress::new(); 74 | let sty = ProgressStyle::with_template( 75 | "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos:>7}/{len:7} ({eta})", 76 | ) 77 | .unwrap() 78 | .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { 79 | write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() 80 | }) 81 | .progress_chars("#>-"); 82 | 83 | let mut threads = vec![]; 84 | for fi in fi_parts { 85 | let count = fi.len(); 86 | let pb = m.add(ProgressBar::new(count as u64)); 87 | pb.set_style(sty.clone()); 88 | 89 | let params = params.clone(); 90 | threads.push(thread::spawn(move || { 91 | let exif_param = ExifWriterParam { 92 | binary_path: params.writer_bin_path.clone(), 93 | }; 94 | 95 | let mut exiftool: Box; 96 | match params.writer_type { 97 | ExifWriterType::Exiftool => { 98 | exiftool = Box::new(ExifWriterExifTool::new(exif_param)); 99 | } 100 | } 101 | 102 | let location_param = LocationReaderParam { 103 | data_path: params.location_file_path.clone(), 104 | time_offset: params.time_offset.clone(), 105 | max_interval: params.location_max_interval.clone(), 106 | }; 107 | 108 | let mut location_reader: Box; 109 | match params.location_reader_type { 110 | LocationReaderType::LifePath => { 111 | location_reader = Box::new(life_path::LocationReaderLiftPath::new(location_param)); 112 | } 113 | } 114 | 115 | let mut now_state = 0; 116 | for file in fi { 117 | now_state += 1; 118 | 119 | let filename = file.as_str(); 120 | let time = exiftool.read_timestamp(filename); 121 | let location = location_reader.get_location(time as i32); 122 | 123 | if location.is_some() { 124 | let mut location = location.unwrap(); 125 | 126 | if params.location_gps_coordinate_target.is_some() { 127 | match params.location_gps_coordinate_target.unwrap() { 128 | LocationGpsCoordinateTarget::GCJ02 => { 129 | let (lat, lon) = wgs_to_gcj(location.lat, location.lon); 130 | location.lat = lat; 131 | location.lon = lon; 132 | } 133 | _ => {} 134 | } 135 | } else { 136 | let result = lc::is_point_in_gcj_region((location.lat, location.lon)); 137 | 138 | if result { 139 | let (lat, lon) = wgs_to_gcj(location.lat, location.lon); 140 | location.lat = lat; 141 | location.lon = lon; 142 | } 143 | } 144 | 145 | exiftool.write_location(filename, location.lat, location.lon, location.alt); 146 | 147 | pb.suspend(|| { 148 | let filename = Path::new(filename).file_name().unwrap().to_str().unwrap(); 149 | info!( 150 | "[{}] Location updated, lat: {}, lon: {}", 151 | filename, location.lat, location.lon 152 | ); 153 | }); 154 | } else { 155 | pb.suspend(|| { 156 | let filename = Path::new(filename).file_name().unwrap().to_str().unwrap(); 157 | warn!("[{}] No matching location, timestamp {}", filename, time); 158 | }); 159 | } 160 | 161 | pb.set_position(now_state); 162 | } 163 | 164 | exiftool.close().expect("Failed to close exif writer"); 165 | })); 166 | } 167 | 168 | for thread in threads { 169 | let _ = thread.join(); 170 | } 171 | m.clear().unwrap(); 172 | info!("Finished"); 173 | 174 | // pb.finish_with_message("Finished"); 175 | } 176 | -------------------------------------------------------------------------------- /src/core/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::path::PathBuf; 3 | use simple_log::log::{debug, error}; 4 | use simple_log::LogConfigBuilder; 5 | 6 | use crate::core::app::{self, ExifWriterType, LocationReaderType, LocationGpsCoordinateTarget}; 7 | 8 | #[derive(Parser)] 9 | #[command(name = "nya-exif")] 10 | #[command(author = "Lyn ")] 11 | #[command(version)] 12 | struct Cli { 13 | /// Path to photography files 14 | path: Vec, 15 | 16 | /// Turn on recursive mode 17 | #[arg(short, long, default_value_t = true)] 18 | recursive: bool, 19 | 20 | /// Threads 21 | /// 22 | /// Number of threads to use. 23 | #[arg(short = 'x', long, default_value_t = 3)] 24 | threads: u32, 25 | 26 | /// Exif writer type 27 | #[arg(short = 'w', long, value_enum, default_value_t = ExifWriterType::Exiftool)] 28 | writer_type: ExifWriterType, 29 | 30 | /// Exif writer binary path 31 | /// 32 | /// Path to the exif writer binary. 33 | /// 34 | /// Leave it blank for the program to search automatically. 35 | #[arg(short = 'b', long)] 36 | writer_bin_path: Option, 37 | 38 | /// Location reader type 39 | /// 40 | /// LiftPath(一生足迹): 41 | /// https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399 42 | /// On MacOS, the program will automatically search for Lifetime Footprint data in the user's iCloud directory. 43 | /// In systems other than MacOS, you need to manually specify the directory. 44 | #[arg(short = 'l', long, value_enum, default_value_t = LocationReaderType::LifePath)] 45 | location_reader_type: LocationReaderType, 46 | 47 | /// Location file path 48 | /// 49 | /// The corresponding location reader's data directory path. Leave it blank for the program to search automatically. 50 | #[arg(short = 'f', long)] 51 | location_file_path: Option, 52 | 53 | /// Location max interval in seconds 54 | /// 55 | /// Specifies the maximum time interval for location data near the photo time. 56 | /// 57 | /// If the difference between the timestamp of the location data and the photo exceeds this value, the location data will not be written. 58 | #[arg(short = 'i', long, default_value_t = 600)] 59 | location_max_interval: u32, 60 | 61 | /// Location GPS coordinate convert target 62 | /// 63 | /// Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. 64 | #[arg(short = 'c', long, value_enum)] 65 | location_coordinate_target: Option, 66 | 67 | /// Overwrite original file 68 | #[arg(short, long, default_value_t = true)] 69 | overwrite_original: bool, 70 | 71 | /// Time offset in seconds 72 | /// 73 | /// Used for situations where the camera time is inconsistent with real time. 74 | /// 75 | /// E.g. the camera time is 1 hour ahead of real time, then fill in 3600 here. 76 | #[arg(short, long, default_value_t = 0)] 77 | time_offset: i32, 78 | 79 | /// Turn on debug mode 80 | #[arg(short, long, default_value_t = false)] 81 | debug: bool, 82 | } 83 | 84 | pub fn run() { 85 | let cli = Cli::parse(); 86 | 87 | let mut param = app::AppParams { 88 | operate_dir: Vec::new(), 89 | recursive: true, 90 | thread_count: 3, 91 | writer_type: app::ExifWriterType::Exiftool, 92 | writer_bin_path: None, 93 | location_reader_type: app::LocationReaderType::LifePath, 94 | location_file_path: None, 95 | location_max_interval: 1800, 96 | location_gps_coordinate_target: None, 97 | overwrite_original: false, 98 | time_offset: 0, 99 | }; 100 | 101 | let config = LogConfigBuilder::builder() 102 | .level(if cli.debug { "debug" } else { "info" }) 103 | .path("./nya-exif.log") 104 | .time_format("%y-%m-%d %H:%M:%S") 105 | .build(); 106 | simple_log::new(config).expect("Failed to init log"); 107 | 108 | if cli.path.len() > 0 { 109 | debug!("[Arg] Value for path: {:?}", cli.path); 110 | param.operate_dir = cli.path.iter().map(|x| PathBuf::from(x)).collect(); 111 | } else { 112 | error!("No path specified"); 113 | std::process::exit(1); 114 | } 115 | 116 | debug!("[Arg] Value for recursive: {}", cli.recursive); 117 | param.recursive = cli.recursive; 118 | 119 | debug!("[Arg] Value for threads: {}", cli.threads); 120 | param.thread_count = cli.threads; 121 | 122 | debug!("[Arg] Value for writer_type: {:?}", cli.writer_type); 123 | param.writer_type = cli.writer_type; 124 | 125 | if let Some(writer_bin_path) = cli.writer_bin_path { 126 | debug!("[Arg] Value for writer_bin_path: {}", writer_bin_path); 127 | param.writer_bin_path = Some(PathBuf::from(writer_bin_path)); 128 | } 129 | 130 | debug!("[Arg] Value for location_reader_type: {:?}", cli.location_reader_type); 131 | param.location_reader_type = cli.location_reader_type; 132 | 133 | if let Some(location_file_path) = cli.location_file_path { 134 | debug!("[Arg] Value for location_file_path: {}", location_file_path); 135 | param.location_file_path = Some(PathBuf::from(location_file_path)); 136 | } 137 | 138 | debug!("[Arg] Value for location_max_interval: {}", cli.location_max_interval); 139 | param.location_max_interval = cli.location_max_interval; 140 | 141 | if let Some(location_coordinate_target) = cli.location_coordinate_target { 142 | debug!("[Arg] Value for location_gps_coordinate_target: {:?}", location_coordinate_target); 143 | param.location_gps_coordinate_target = Some(location_coordinate_target); 144 | } 145 | 146 | debug!("[Arg] Value for overwrite_original: {}", cli.overwrite_original); 147 | param.overwrite_original = cli.overwrite_original; 148 | 149 | debug!("[Arg] Value for time_offset: {}", cli.time_offset); 150 | param.time_offset = cli.time_offset; 151 | 152 | debug!("[Arg] Value for app params: {:?}", param); 153 | 154 | app::run(param); 155 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nya-exif 2 | 3 | 中文 | English 4 | 5 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/LynMoe/nya-exif) 6 | 7 | ## 介绍 8 | 9 | `nya-exif` 是一个用于匹配照片 GPS 信息, 并写入文件 EXIF 信息的工具, 支持 JPEG 和 PNG 及各大相机厂商的主流RAW格式. 本工具基于 Rust 编写, 支持全平台使用 10 | 11 | ## Features 12 | 13 | - [x] 支持 JPEG 和 PNG 及各大相机厂商的主流RAW格式 14 | - [x] 全平台支持 15 | - [x] 支持国策局 GCJ-02 和 WGS-84 坐标系 (解决国内坐标漂移问题) 16 | - [x] 自动检测国内外位置, 自动转换为对应坐标系 17 | - [x] 支持多线程处理 18 | 19 | ## DEMO 20 | 21 | ```shell 22 | ➜ nya-exif /path/to/image/folder/ 23 | 2023-11-11 12:46:08.337698000 [INFO] :[20230908-_MGL4100.JPG] Location updated, lat: 34.7737885, lon: 131.9007701 24 | 2023-11-11 12:46:08.394225000 [INFO] :[20230908-_MGL4114.JPG] Location updated, lat: 34.67844170666667, lon: 131.83647663733333 25 | 2023-11-11 12:46:08.434180000 [INFO] :[20230908-_MGL4128.JPG] Location updated, lat: 34.68192337279844, lon: 131.8327970596869 26 | ⠉ [00:00:03] [###########################>-----------------------------------------------] 54/77 (1.8s) 27 | ⠉ [00:00:03] [###########################>-----------------------------------------------] 55/77 (1.6s) 28 | ⠉ [00:00:03] [#############################>---------------------------------------------] 60/79 (1.4s) 29 | ``` 30 | 31 | ## 使用 32 | 33 | 确保已安装 [ExifTool](https://exiftool.org/), 并添加至 PATH 34 | 35 | ```shell 36 | # macOS 下, 一生足迹启动 iCloud 云备份, 可直接运行 37 | nya-exif . 38 | 39 | # 其他平台下, 需要将一生足迹数据目录拷贝至本地 40 | nya-exif -f /path/to/life-path/data /path/to/images 41 | 42 | # 若 ExifTool 安装路径不在 PATH 中, 手动指定可执行文件位置 43 | nya-exif -b /path/to/exiftool /path/to/images 44 | 45 | # 指定目标坐标系, 默认为自动检测, 如果在边境线附近需要手动指定 46 | nya-exif -c wgs84 /path/to/images 47 | ``` 48 | 49 | > [!NOTE] 50 | > 推荐在本地文件上运行程序, 若在网络盘上运行, 会影响程序速度 51 | 52 | ## ExifWriter/LocationReader 支持情况 53 | 54 | | Exif Writer | 描述 | 55 | | --- | --- | 56 | | [ExifTool](https://exiftool.org/) | 支持大多数图片文件的EXIF信息读写, 全平台支持
**安装**: [官网](https://exiftool.org/)下载对应平台安装包, 直接安装即可, 添加至PATH后本工具会自动查找程序路径; 若未添加到 PATH, 需要手动指定 `-b` 选项到程序的二进制文件绝对路径 | 57 | 58 | | Location Reader | 描述 | 59 | | --- | --- | 60 | | [一生足迹](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399) | 一生足迹是一款 iOS 端记录用户足迹的应用, 耗电量较低, 可常驻后台
**安装:** [App Store](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399)下载安装即可, 需要开启 iCloud 同步
**使用:** 对于 macOS 用户, 程序会自动查找一生足迹在 iCloud 中的备份位置; 对于其他平台用户, 需要手动指定目录(含`backUpData.csv`文件)的位置
**注意:** 该备份文件的同步不是很及时, 如果拍摄时间较新需要在 App 中手动到处数据文件加载 | 61 | 62 | 目前程序默认选项为 `ExifTool` + `一生足迹`, 在 macOS 平台安装 `ExifTool.pkg` 后可直接使用默认参数启动工具 63 | 64 | ## 命令行参数 65 | 66 | ```shell 67 | Usage: nya-exif [OPTIONS] [PATH] 68 | 69 | Arguments: 70 | [PATH] 71 | Path to photography files 72 | 73 | Options: 74 | -r, --recursive 75 | Turn on recursive mode 76 | 77 | -x, --threads 78 | Threads 79 | 80 | Number of threads to use. 81 | 82 | [default: 3] 83 | 84 | -w, --writer-type 85 | Exif writer type 86 | 87 | [default: exiftool] 88 | 89 | Possible values: 90 | - exiftool: ExitTool(https://exiftool.org/) 91 | 92 | -b, --writer-bin-path 93 | Exif writer binary path 94 | 95 | Path to the exif writer binary. 96 | 97 | Leave it blank for the program to search automatically. 98 | 99 | -l, --location-reader-type 100 | Location reader type 101 | 102 | LiftPath(一生足迹): https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399 On MacOS, the program will automatically search for Lifetime Footprint data in the user's iCloud directory. In systems other than MacOS, you need to manually specify the directory. 103 | 104 | [default: life-path] 105 | 106 | Possible values: 107 | - life-path: LifePath(一生足迹) 108 | 109 | -f, --location-file-path 110 | Location file path 111 | 112 | The corresponding location reader's data directory path. Leave it blank for the program to search automatically. 113 | 114 | -i, --location-max-interval 115 | Location max interval in seconds 116 | 117 | Specifies the maximum time interval for location data near the photo time. 118 | 119 | If the difference between the timestamp of the location data and the photo exceeds this value, the location data will not be written. 120 | 121 | [default: 600] 122 | 123 | -c, --location-coordinate-target 124 | Location GPS coordinate convert target 125 | 126 | Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. 127 | 128 | Possible values: 129 | - wgs84: Global coordinate system 130 | - gcj02: China coordinate system 131 | 132 | -o, --overwrite-original 133 | Overwrite original file 134 | 135 | -t, --time-offset 136 | Time offset in seconds 137 | 138 | Used for situations where the camera time is inconsistent with real time. 139 | 140 | E.g. the camera time is 1 hour ahead of real time, then fill in 3600 here. 141 | 142 | [default: 0] 143 | 144 | -d, --debug 145 | Turn on debug mode 146 | 147 | -h, --help 148 | Print help (see a summary with '-h') 149 | 150 | -V, --version 151 | Print version 152 | ``` 153 | 154 | ## Contributing 155 | 156 | 若有其他的位置记录软件需要支持, 欢迎提交 PR 或附上详细数据信息提交 Issue 157 | 158 | 若遇到 ExifTool 无法处理的文件, 请附上文件信息提交 Issue 159 | 160 | 对于 ExifWriter 和 LocationReader, 请参考 `src/exif_writer` 和 `src/location_reader` 目录下已有的实现, 实现对应的 Trait 后在 `src/core/app.rs` 中注册即可 161 | 162 | > [!IMPORTANT] 163 | > Location Reader返回的经纬度应该为地球坐标系(WGS84), 本工具会根据用户选择的坐标系进行转换 164 | 165 | ## Declaimer 166 | 167 | 本工具中附带的 `GCJ-02` 范围数据仅用于粗略地理位置判断, 不具有任何立场和政治倾向, 请勿用于其他用途 168 | 169 | ## License 170 | 171 | [MIT](LICENSE) ©[Lyn](mailto://i@lyn.moe) 172 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # nya-exif 2 | 3 | 中文 | English 4 | 5 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/LynMoe/nya-exif) 6 | 7 | ## Introduction 8 | 9 | `nya-exif` is a versatile tool designed to match and write photo GPS data into file EXIF information. It supports JPEG, PNG, and major camera manufacturers' mainstream RAW formats. Developed in Rust, this tool is compatible with all platforms. 10 | 11 | ## Features 12 | 13 | - [x] Supports JPEG and PNG as well as mainstream RAW formats from major camera manufacturers 14 | - [x] Multi-platform support 15 | - [x] Supports GCJ-02 and WGS-84 coordinate systems (defaults to GCJ-02) 16 | - [x] Automatically detects location data and converts it to the corresponding coordinate system 17 | - [x] Multi-threaded processing 18 | 19 | ## DEMO 20 | 21 | ```shell 22 | ➜ nya-exif /path/to/image/folder/ 23 | 2023-11-11 12:46:08.337698000 [INFO] :[20230908-_MGL4100.JPG] Location updated, lat: 34.7737885, lon: 131.9007701 24 | 2023-11-11 12:46:08.394225000 [INFO] :[20230908-_MGL4114.JPG] Location updated, lat: 34.67844170666667, lon: 131.83647663733333 25 | 2023-11-11 12:46:08.434180000 [INFO] :[20230908-_MGL4128.JPG] Location updated, lat: 34.68192337279844, lon: 131.8327970596869 26 | ⠉ [00:00:03] [###########################>-----------------------------------------------] 54/77 (1.8s) 27 | ⠉ [00:00:03] [###########################>-----------------------------------------------] 55/77 (1.6s) 28 | ⠉ [00:00:03] [#############################>---------------------------------------------] 60/79 (1.4s) 29 | ``` 30 | 31 | ## Usage 32 | 33 | Ensure [ExifTool](https://exiftool.org/) is installed and added to PATH. 34 | 35 | ```shell 36 | # On macOS, you can directly run the "Lifetime Footprints" to start iCloud backup. 37 | nya-exif /path/to/images 38 | 39 | # On other platforms, you need to copy the lifetime footprint data directory to local. 40 | nya-exif -f /path/to/life-path/data /path/to/images 41 | 42 | # If the ExifTool installation path is not in PATH, manually specify the executable file location. 43 | nya-exif -b /path/to/exiftool /path/to/images 44 | 45 | # Specify the target coordinate system, default is China's GCJ-02 coordinate system. 46 | nya-exif -c wgs84 /path/to/images 47 | ``` 48 | 49 | > [!NOTE] 50 | > It is recommended to run the program on local files. If running on a network drive, the speed of the program will be affected. 51 | 52 | ## ExifWriter/LocationReader Table 53 | 54 | | Exif Writer | Description | 55 | | --- | --- | 56 | | [ExifTool](https://exiftool.org/) | Supports reading and writing EXIF information for most image files, supported across all platforms
**Installation**: Download the corresponding platform installation package from the [official website](https://exiftool.org/), install directly. After adding to PATH, this tool will automatically search for the program path. | 57 | 58 | | Location Reader | Description | 59 | | --- | --- | 60 | | [一生足迹](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399) | "Life Path" is an iOS application that records user's footprints, with low power consumption and can stay in the background.
**Installation**:[App Store](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399) Download and install, need to enable iCloud sync
**Usage**: For macOS users, the program will automatically find the backup location of life's footprints in iCloud; for users of other platforms, you need to manually specify the directory (containing `backUpData.csv` file) location. | 61 | 62 | The current program default option is `ExifTool` + `Lifetime Footprints`. After installing `ExifTool.pkg` on the macOS platform, you can directly use the default parameters to start. 63 | 64 | ## Command line parameters. 65 | 66 | ```shell 67 | Usage: nya-exif [OPTIONS] [PATH] 68 | 69 | Arguments: 70 | [PATH] 71 | Path to photography files 72 | 73 | Options: 74 | -r, --recursive 75 | Turn on recursive mode 76 | 77 | -x, --threads 78 | Threads 79 | 80 | Number of threads to use. 81 | 82 | [default: 3] 83 | 84 | -w, --writer-type 85 | Exif writer type 86 | 87 | [default: exiftool] 88 | 89 | Possible values: 90 | - exiftool: ExitTool(https://exiftool.org/) 91 | 92 | -b, --writer-bin-path 93 | Exif writer binary path 94 | 95 | Path to the exif writer binary. 96 | 97 | Leave it blank for the program to search automatically. 98 | 99 | -l, --location-reader-type 100 | Location reader type 101 | 102 | LiftPath(一生足迹): https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399 On MacOS, the program will automatically search for Lifetime Footprint data in the user's iCloud directory. In systems other than MacOS, you need to manually specify the directory. 103 | 104 | [default: life-path] 105 | 106 | Possible values: 107 | - life-path: LifePath(一生足迹) 108 | 109 | -f, --location-file-path 110 | Location file path 111 | 112 | The corresponding location reader's data directory path. Leave it blank for the program to search automatically. 113 | 114 | -i, --location-max-interval 115 | Location max interval in seconds 116 | 117 | Specifies the maximum time interval for location data near the photo time. 118 | 119 | If the difference between the timestamp of the location data and the photo exceeds this value, the location data will not be written. 120 | 121 | [default: 600] 122 | 123 | -c, --location-coordinate-target 124 | Location GPS coordinate convert target 125 | 126 | Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. 127 | 128 | Possible values: 129 | - wgs84: Global coordinate system 130 | - gcj02: China coordinate system 131 | 132 | -o, --overwrite-original 133 | Overwrite original file 134 | 135 | -t, --time-offset 136 | Time offset in seconds 137 | 138 | Used for situations where the camera time is inconsistent with real time. 139 | 140 | E.g. the camera time is 1 hour ahead of real time, then fill in 3600 here. 141 | 142 | [default: 0] 143 | 144 | -d, --debug 145 | Turn on debug mode 146 | 147 | -h, --help 148 | Print help (see a summary with '-h') 149 | 150 | -V, --version 151 | Print version 152 | ``` 153 | 154 | ## Contributing 155 | 156 | If there are other location recording software that need support, feel free to submit a PR or attach detailed data information to submit an Issue. 157 | 158 | If you encounter files that ExifTool cannot handle, please attach the file and submit an Issue. 159 | 160 | For ExifWriter and LocationReader, please refer to the existing implementations in the `src/exif_writer` and `src/location_reader` directories. After implementing the corresponding Trait, register it in `src/core/app.rs`. 161 | 162 | > [!IMPORTANT] 163 | > The latitude and longitude returned by the Location Reader should be in the Earth coordinate system (WGS84), this tool will convert according to the coordinate system selected by the user. 164 | 165 | ## License 166 | 167 | [MIT](LICENSE) ©[Lyn](mailto://i@lyn.moe) 168 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "android-tzdata" 13 | version = "0.1.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 16 | 17 | [[package]] 18 | name = "android_system_properties" 19 | version = "0.1.5" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 22 | dependencies = [ 23 | "libc", 24 | ] 25 | 26 | [[package]] 27 | name = "anstream" 28 | version = "0.6.4" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 31 | dependencies = [ 32 | "anstyle", 33 | "anstyle-parse", 34 | "anstyle-query", 35 | "anstyle-wincon", 36 | "colorchoice", 37 | "utf8parse", 38 | ] 39 | 40 | [[package]] 41 | name = "anstyle" 42 | version = "1.0.4" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 45 | 46 | [[package]] 47 | name = "anstyle-parse" 48 | version = "0.2.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 51 | dependencies = [ 52 | "utf8parse", 53 | ] 54 | 55 | [[package]] 56 | name = "anstyle-query" 57 | version = "1.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 60 | dependencies = [ 61 | "windows-sys 0.48.0", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle-wincon" 66 | version = "3.0.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 69 | dependencies = [ 70 | "anstyle", 71 | "windows-sys 0.48.0", 72 | ] 73 | 74 | [[package]] 75 | name = "arc-swap" 76 | version = "0.4.8" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" 79 | 80 | [[package]] 81 | name = "autocfg" 82 | version = "1.1.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "1.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "2.4.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 97 | 98 | [[package]] 99 | name = "bumpalo" 100 | version = "3.14.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 103 | 104 | [[package]] 105 | name = "cc" 106 | version = "1.0.83" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 109 | dependencies = [ 110 | "libc", 111 | ] 112 | 113 | [[package]] 114 | name = "cfg-if" 115 | version = "1.0.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 118 | 119 | [[package]] 120 | name = "chrono" 121 | version = "0.4.31" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 124 | dependencies = [ 125 | "android-tzdata", 126 | "iana-time-zone", 127 | "js-sys", 128 | "num-traits", 129 | "wasm-bindgen", 130 | "windows-targets 0.48.5", 131 | ] 132 | 133 | [[package]] 134 | name = "clap" 135 | version = "4.4.7" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" 138 | dependencies = [ 139 | "clap_builder", 140 | "clap_derive", 141 | ] 142 | 143 | [[package]] 144 | name = "clap_builder" 145 | version = "4.4.7" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" 148 | dependencies = [ 149 | "anstream", 150 | "anstyle", 151 | "clap_lex", 152 | "strsim", 153 | ] 154 | 155 | [[package]] 156 | name = "clap_derive" 157 | version = "4.4.7" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 160 | dependencies = [ 161 | "heck", 162 | "proc-macro2", 163 | "quote", 164 | "syn", 165 | ] 166 | 167 | [[package]] 168 | name = "clap_lex" 169 | version = "0.6.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 172 | 173 | [[package]] 174 | name = "colorchoice" 175 | version = "1.0.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 178 | 179 | [[package]] 180 | name = "console" 181 | version = "0.15.7" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 184 | dependencies = [ 185 | "encode_unicode", 186 | "lazy_static", 187 | "libc", 188 | "unicode-width", 189 | "windows-sys 0.45.0", 190 | ] 191 | 192 | [[package]] 193 | name = "convert_case" 194 | version = "0.5.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" 197 | 198 | [[package]] 199 | name = "core-foundation-sys" 200 | version = "0.8.4" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 203 | 204 | [[package]] 205 | name = "crc32fast" 206 | version = "1.3.2" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 209 | dependencies = [ 210 | "cfg-if", 211 | ] 212 | 213 | [[package]] 214 | name = "csv" 215 | version = "1.3.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 218 | dependencies = [ 219 | "csv-core", 220 | "itoa", 221 | "ryu", 222 | "serde", 223 | ] 224 | 225 | [[package]] 226 | name = "csv-core" 227 | version = "0.1.11" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 230 | dependencies = [ 231 | "memchr", 232 | ] 233 | 234 | [[package]] 235 | name = "dirs" 236 | version = "5.0.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 239 | dependencies = [ 240 | "dirs-sys", 241 | ] 242 | 243 | [[package]] 244 | name = "dirs-sys" 245 | version = "0.4.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 248 | dependencies = [ 249 | "libc", 250 | "option-ext", 251 | "redox_users", 252 | "windows-sys 0.48.0", 253 | ] 254 | 255 | [[package]] 256 | name = "either" 257 | version = "1.9.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 260 | 261 | [[package]] 262 | name = "encode_unicode" 263 | version = "0.3.6" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 266 | 267 | [[package]] 268 | name = "errno" 269 | version = "0.3.5" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 272 | dependencies = [ 273 | "libc", 274 | "windows-sys 0.48.0", 275 | ] 276 | 277 | [[package]] 278 | name = "flate2" 279 | version = "1.0.28" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 282 | dependencies = [ 283 | "crc32fast", 284 | "miniz_oxide", 285 | ] 286 | 287 | [[package]] 288 | name = "fnv" 289 | version = "1.0.7" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 292 | 293 | [[package]] 294 | name = "getrandom" 295 | version = "0.2.10" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 298 | dependencies = [ 299 | "cfg-if", 300 | "libc", 301 | "wasi", 302 | ] 303 | 304 | [[package]] 305 | name = "hashbrown" 306 | version = "0.12.3" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 309 | 310 | [[package]] 311 | name = "heck" 312 | version = "0.4.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 315 | 316 | [[package]] 317 | name = "home" 318 | version = "0.5.5" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 321 | dependencies = [ 322 | "windows-sys 0.48.0", 323 | ] 324 | 325 | [[package]] 326 | name = "humantime" 327 | version = "1.3.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 330 | dependencies = [ 331 | "quick-error", 332 | ] 333 | 334 | [[package]] 335 | name = "iana-time-zone" 336 | version = "0.1.58" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 339 | dependencies = [ 340 | "android_system_properties", 341 | "core-foundation-sys", 342 | "iana-time-zone-haiku", 343 | "js-sys", 344 | "wasm-bindgen", 345 | "windows-core", 346 | ] 347 | 348 | [[package]] 349 | name = "iana-time-zone-haiku" 350 | version = "0.1.2" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 353 | dependencies = [ 354 | "cc", 355 | ] 356 | 357 | [[package]] 358 | name = "indexmap" 359 | version = "1.9.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 362 | dependencies = [ 363 | "autocfg", 364 | "hashbrown", 365 | ] 366 | 367 | [[package]] 368 | name = "indicatif" 369 | version = "0.17.7" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" 372 | dependencies = [ 373 | "console", 374 | "instant", 375 | "number_prefix", 376 | "portable-atomic", 377 | "unicode-width", 378 | ] 379 | 380 | [[package]] 381 | name = "instant" 382 | version = "0.1.12" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 385 | dependencies = [ 386 | "cfg-if", 387 | ] 388 | 389 | [[package]] 390 | name = "is_debug" 391 | version = "1.0.1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" 394 | 395 | [[package]] 396 | name = "itoa" 397 | version = "1.0.9" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 400 | 401 | [[package]] 402 | name = "js-sys" 403 | version = "0.3.65" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 406 | dependencies = [ 407 | "wasm-bindgen", 408 | ] 409 | 410 | [[package]] 411 | name = "lazy_static" 412 | version = "1.4.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 415 | 416 | [[package]] 417 | name = "libc" 418 | version = "0.2.150" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 421 | 422 | [[package]] 423 | name = "libredox" 424 | version = "0.0.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 427 | dependencies = [ 428 | "bitflags 2.4.1", 429 | "libc", 430 | "redox_syscall 0.4.1", 431 | ] 432 | 433 | [[package]] 434 | name = "linked-hash-map" 435 | version = "0.5.6" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 438 | 439 | [[package]] 440 | name = "linux-raw-sys" 441 | version = "0.4.10" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 444 | 445 | [[package]] 446 | name = "lock_api" 447 | version = "0.4.11" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 450 | dependencies = [ 451 | "autocfg", 452 | "scopeguard", 453 | ] 454 | 455 | [[package]] 456 | name = "log" 457 | version = "0.4.20" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 460 | dependencies = [ 461 | "serde", 462 | ] 463 | 464 | [[package]] 465 | name = "log-mdc" 466 | version = "0.1.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" 469 | 470 | [[package]] 471 | name = "log4rs" 472 | version = "0.13.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "e1e1ad45e4584824d760c35d71868dd7e6e5acd8f5195a9573743b369fc86cd6" 475 | dependencies = [ 476 | "arc-swap", 477 | "chrono", 478 | "flate2", 479 | "fnv", 480 | "humantime", 481 | "libc", 482 | "log", 483 | "log-mdc", 484 | "parking_lot", 485 | "serde", 486 | "serde-value", 487 | "serde_derive", 488 | "serde_json", 489 | "serde_yaml", 490 | "thread-id", 491 | "winapi", 492 | ] 493 | 494 | [[package]] 495 | name = "memchr" 496 | version = "2.6.4" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 499 | 500 | [[package]] 501 | name = "miniz_oxide" 502 | version = "0.7.1" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 505 | dependencies = [ 506 | "adler", 507 | ] 508 | 509 | [[package]] 510 | name = "num-traits" 511 | version = "0.2.17" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 514 | dependencies = [ 515 | "autocfg", 516 | ] 517 | 518 | [[package]] 519 | name = "number_prefix" 520 | version = "0.4.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 523 | 524 | [[package]] 525 | name = "nya-exif" 526 | version = "0.1.3" 527 | dependencies = [ 528 | "chrono", 529 | "clap", 530 | "csv", 531 | "dirs", 532 | "indicatif", 533 | "os_info", 534 | "simple-log", 535 | "undrift_gps", 536 | "which", 537 | ] 538 | 539 | [[package]] 540 | name = "once_cell" 541 | version = "1.18.0" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 544 | 545 | [[package]] 546 | name = "option-ext" 547 | version = "0.2.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 550 | 551 | [[package]] 552 | name = "ordered-float" 553 | version = "1.1.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" 556 | dependencies = [ 557 | "num-traits", 558 | ] 559 | 560 | [[package]] 561 | name = "os_info" 562 | version = "3.7.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" 565 | dependencies = [ 566 | "log", 567 | "serde", 568 | "winapi", 569 | ] 570 | 571 | [[package]] 572 | name = "parking_lot" 573 | version = "0.11.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 576 | dependencies = [ 577 | "instant", 578 | "lock_api", 579 | "parking_lot_core", 580 | ] 581 | 582 | [[package]] 583 | name = "parking_lot_core" 584 | version = "0.8.6" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" 587 | dependencies = [ 588 | "cfg-if", 589 | "instant", 590 | "libc", 591 | "redox_syscall 0.2.16", 592 | "smallvec", 593 | "winapi", 594 | ] 595 | 596 | [[package]] 597 | name = "portable-atomic" 598 | version = "1.5.1" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" 601 | 602 | [[package]] 603 | name = "proc-macro2" 604 | version = "1.0.69" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 607 | dependencies = [ 608 | "unicode-ident", 609 | ] 610 | 611 | [[package]] 612 | name = "quick-error" 613 | version = "1.2.3" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 616 | 617 | [[package]] 618 | name = "quote" 619 | version = "1.0.33" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 622 | dependencies = [ 623 | "proc-macro2", 624 | ] 625 | 626 | [[package]] 627 | name = "redox_syscall" 628 | version = "0.1.57" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 631 | 632 | [[package]] 633 | name = "redox_syscall" 634 | version = "0.2.16" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 637 | dependencies = [ 638 | "bitflags 1.3.2", 639 | ] 640 | 641 | [[package]] 642 | name = "redox_syscall" 643 | version = "0.4.1" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 646 | dependencies = [ 647 | "bitflags 1.3.2", 648 | ] 649 | 650 | [[package]] 651 | name = "redox_users" 652 | version = "0.4.4" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 655 | dependencies = [ 656 | "getrandom", 657 | "libredox", 658 | "thiserror", 659 | ] 660 | 661 | [[package]] 662 | name = "rustix" 663 | version = "0.38.21" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 666 | dependencies = [ 667 | "bitflags 2.4.1", 668 | "errno", 669 | "libc", 670 | "linux-raw-sys", 671 | "windows-sys 0.48.0", 672 | ] 673 | 674 | [[package]] 675 | name = "ryu" 676 | version = "1.0.15" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 679 | 680 | [[package]] 681 | name = "scopeguard" 682 | version = "1.2.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 685 | 686 | [[package]] 687 | name = "serde" 688 | version = "1.0.190" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" 691 | dependencies = [ 692 | "serde_derive", 693 | ] 694 | 695 | [[package]] 696 | name = "serde-value" 697 | version = "0.6.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "5a65a7291a8a568adcae4c10a677ebcedbc6c9cec91c054dee2ce40b0e3290eb" 700 | dependencies = [ 701 | "ordered-float", 702 | "serde", 703 | ] 704 | 705 | [[package]] 706 | name = "serde_derive" 707 | version = "1.0.190" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" 710 | dependencies = [ 711 | "proc-macro2", 712 | "quote", 713 | "syn", 714 | ] 715 | 716 | [[package]] 717 | name = "serde_json" 718 | version = "1.0.108" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 721 | dependencies = [ 722 | "itoa", 723 | "ryu", 724 | "serde", 725 | ] 726 | 727 | [[package]] 728 | name = "serde_yaml" 729 | version = "0.8.26" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" 732 | dependencies = [ 733 | "indexmap", 734 | "ryu", 735 | "serde", 736 | "yaml-rust", 737 | ] 738 | 739 | [[package]] 740 | name = "simple-log" 741 | version = "1.6.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "110feefe2a808cadb7fe8c07c615ac82e899f1795fec063ed2fb1d72de5d417b" 744 | dependencies = [ 745 | "convert_case", 746 | "is_debug", 747 | "log", 748 | "log4rs", 749 | "once_cell", 750 | "serde", 751 | ] 752 | 753 | [[package]] 754 | name = "smallvec" 755 | version = "1.11.1" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 758 | 759 | [[package]] 760 | name = "strsim" 761 | version = "0.10.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 764 | 765 | [[package]] 766 | name = "syn" 767 | version = "2.0.39" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 770 | dependencies = [ 771 | "proc-macro2", 772 | "quote", 773 | "unicode-ident", 774 | ] 775 | 776 | [[package]] 777 | name = "thiserror" 778 | version = "1.0.50" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 781 | dependencies = [ 782 | "thiserror-impl", 783 | ] 784 | 785 | [[package]] 786 | name = "thiserror-impl" 787 | version = "1.0.50" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 790 | dependencies = [ 791 | "proc-macro2", 792 | "quote", 793 | "syn", 794 | ] 795 | 796 | [[package]] 797 | name = "thread-id" 798 | version = "3.3.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" 801 | dependencies = [ 802 | "libc", 803 | "redox_syscall 0.1.57", 804 | "winapi", 805 | ] 806 | 807 | [[package]] 808 | name = "undrift_gps" 809 | version = "0.3.1" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "802ac703aef8466fb9c498da7ccd0a33aa7cf5ccc26741df00007ee112a158a8" 812 | 813 | [[package]] 814 | name = "unicode-ident" 815 | version = "1.0.12" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 818 | 819 | [[package]] 820 | name = "unicode-width" 821 | version = "0.1.11" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 824 | 825 | [[package]] 826 | name = "utf8parse" 827 | version = "0.2.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 830 | 831 | [[package]] 832 | name = "wasi" 833 | version = "0.11.0+wasi-snapshot-preview1" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 836 | 837 | [[package]] 838 | name = "wasm-bindgen" 839 | version = "0.2.88" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 842 | dependencies = [ 843 | "cfg-if", 844 | "wasm-bindgen-macro", 845 | ] 846 | 847 | [[package]] 848 | name = "wasm-bindgen-backend" 849 | version = "0.2.88" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 852 | dependencies = [ 853 | "bumpalo", 854 | "log", 855 | "once_cell", 856 | "proc-macro2", 857 | "quote", 858 | "syn", 859 | "wasm-bindgen-shared", 860 | ] 861 | 862 | [[package]] 863 | name = "wasm-bindgen-macro" 864 | version = "0.2.88" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 867 | dependencies = [ 868 | "quote", 869 | "wasm-bindgen-macro-support", 870 | ] 871 | 872 | [[package]] 873 | name = "wasm-bindgen-macro-support" 874 | version = "0.2.88" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 877 | dependencies = [ 878 | "proc-macro2", 879 | "quote", 880 | "syn", 881 | "wasm-bindgen-backend", 882 | "wasm-bindgen-shared", 883 | ] 884 | 885 | [[package]] 886 | name = "wasm-bindgen-shared" 887 | version = "0.2.88" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 890 | 891 | [[package]] 892 | name = "which" 893 | version = "5.0.0" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" 896 | dependencies = [ 897 | "either", 898 | "home", 899 | "once_cell", 900 | "rustix", 901 | "windows-sys 0.48.0", 902 | ] 903 | 904 | [[package]] 905 | name = "winapi" 906 | version = "0.3.9" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 909 | dependencies = [ 910 | "winapi-i686-pc-windows-gnu", 911 | "winapi-x86_64-pc-windows-gnu", 912 | ] 913 | 914 | [[package]] 915 | name = "winapi-i686-pc-windows-gnu" 916 | version = "0.4.0" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 919 | 920 | [[package]] 921 | name = "winapi-x86_64-pc-windows-gnu" 922 | version = "0.4.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 925 | 926 | [[package]] 927 | name = "windows-core" 928 | version = "0.51.1" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 931 | dependencies = [ 932 | "windows-targets 0.48.5", 933 | ] 934 | 935 | [[package]] 936 | name = "windows-sys" 937 | version = "0.45.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 940 | dependencies = [ 941 | "windows-targets 0.42.2", 942 | ] 943 | 944 | [[package]] 945 | name = "windows-sys" 946 | version = "0.48.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 949 | dependencies = [ 950 | "windows-targets 0.48.5", 951 | ] 952 | 953 | [[package]] 954 | name = "windows-targets" 955 | version = "0.42.2" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 958 | dependencies = [ 959 | "windows_aarch64_gnullvm 0.42.2", 960 | "windows_aarch64_msvc 0.42.2", 961 | "windows_i686_gnu 0.42.2", 962 | "windows_i686_msvc 0.42.2", 963 | "windows_x86_64_gnu 0.42.2", 964 | "windows_x86_64_gnullvm 0.42.2", 965 | "windows_x86_64_msvc 0.42.2", 966 | ] 967 | 968 | [[package]] 969 | name = "windows-targets" 970 | version = "0.48.5" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 973 | dependencies = [ 974 | "windows_aarch64_gnullvm 0.48.5", 975 | "windows_aarch64_msvc 0.48.5", 976 | "windows_i686_gnu 0.48.5", 977 | "windows_i686_msvc 0.48.5", 978 | "windows_x86_64_gnu 0.48.5", 979 | "windows_x86_64_gnullvm 0.48.5", 980 | "windows_x86_64_msvc 0.48.5", 981 | ] 982 | 983 | [[package]] 984 | name = "windows_aarch64_gnullvm" 985 | version = "0.42.2" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 988 | 989 | [[package]] 990 | name = "windows_aarch64_gnullvm" 991 | version = "0.48.5" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 994 | 995 | [[package]] 996 | name = "windows_aarch64_msvc" 997 | version = "0.42.2" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1000 | 1001 | [[package]] 1002 | name = "windows_aarch64_msvc" 1003 | version = "0.48.5" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1006 | 1007 | [[package]] 1008 | name = "windows_i686_gnu" 1009 | version = "0.42.2" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1012 | 1013 | [[package]] 1014 | name = "windows_i686_gnu" 1015 | version = "0.48.5" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1018 | 1019 | [[package]] 1020 | name = "windows_i686_msvc" 1021 | version = "0.42.2" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1024 | 1025 | [[package]] 1026 | name = "windows_i686_msvc" 1027 | version = "0.48.5" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1030 | 1031 | [[package]] 1032 | name = "windows_x86_64_gnu" 1033 | version = "0.42.2" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1036 | 1037 | [[package]] 1038 | name = "windows_x86_64_gnu" 1039 | version = "0.48.5" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1042 | 1043 | [[package]] 1044 | name = "windows_x86_64_gnullvm" 1045 | version = "0.42.2" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1048 | 1049 | [[package]] 1050 | name = "windows_x86_64_gnullvm" 1051 | version = "0.48.5" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1054 | 1055 | [[package]] 1056 | name = "windows_x86_64_msvc" 1057 | version = "0.42.2" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1060 | 1061 | [[package]] 1062 | name = "windows_x86_64_msvc" 1063 | version = "0.48.5" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1066 | 1067 | [[package]] 1068 | name = "yaml-rust" 1069 | version = "0.4.5" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1072 | dependencies = [ 1073 | "linked-hash-map", 1074 | ] 1075 | --------------------------------------------------------------------------------