Skip to content

Commit

Permalink
feat: support GCJ-02 coordinate system
Browse files Browse the repository at this point in the history
fix(location_reader/life_path): wrong records
doc: update document
chore: release v0.1.2
  • Loading branch information
LynMoe committed Nov 9, 2023
1 parent 4dfa318 commit a3cdc97
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 19 deletions.
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nya-exif"
version = "0.1.0"
version = "0.1.2"
edition = "2021"
authors = ["Lyn <i@lyn.moe>"]
description = "A cross-platform tool for embedding GPS data into photographs"
Expand All @@ -15,4 +15,5 @@ dirs = "5.0.1"
indicatif = "0.17.7"
os_info = "3.7.0"
simple-log = "1.6.0"
undrift_gps = "0.3.1"
which = "5.0.0"
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@

中文 | <a href="README_en.md">English</a>

![GitHub release (with filter)](https://img.shields.io/github/v/release/LynMoe/nya-exif)

## 介绍

`nya-exif` 是一个用于匹配照片 GPS 信息, 并写入文件 EXIF 信息的工具, 支持 JPEG 和 PNG 及各大相机厂商的主流RAW格式. 本工具基于 Rust 编写, 支持全平台使用

## Features

- [x] 支持 JPEG 和 PNG 及各大相机厂商的主流RAW格式
- [x] 全平台支持
- [x] 支持国策局 GCJ-02 和 WGS-84 坐标系 (解决国内坐标漂移问题)

## DEMO

```shell
➜ nya-exif /path/to/image/folder/
2023-11-08 15:57:30.830962000 [INFO] <nya_exif::core::app:84>:Updating location for 20230908-_MGL4076.JPG
2023-11-08 15:57:30.931190000 [INFO] <nya_exif::core::app:84>:Updating location for 20230908-_MGL4062.JPG
2023-11-08 15:57:30.967376000 [INFO] <nya_exif::core::app:84>:Updating location for 20230908-_MGL4089.JPG
2023-11-08 15:57:30.967376000 [WARN] <nya_exif::core::app:120>:Missing location for file _MGL9572.JPG, timestamp 1699257194
⠂ [00:00:04] [###########################>-----------------------------------------------] 93/233 (6.7s)
```
## 使用
确保已安装 [ExifTool](https://exiftool.org/), 并添加至 PATH
```shell
# macOS 下, 一生足迹启动 iCloud 云备份, 可直接运行
nya-exif /path/to/images
Expand All @@ -27,8 +38,14 @@ nya-exif -f /path/to/life-path/data /path/to/images

# 若 ExifTool 安装路径不在 PATH 中, 手动指定可执行文件位置
nya-exif -b /path/to/exiftool /path/to/images

# 指定目标坐标系, 默认为中国 GCJ-02 坐标系, 如果照片拍摄地为海外需要指定为 WGS-84 坐标系
nya-exif -c wgs84 /path/to/images
```
> [!NOTE]
> 推荐在本地文件上运行程序, 若在网络盘上运行, 会影响程序速度
## ExifWriter/LocationReader 支持情况
| Exif Writer | 描述 |
Expand All @@ -37,7 +54,7 @@ nya-exif -b /path/to/exiftool /path/to/images
| Location Reader | 描述 |
| --- | --- |
| [一生足迹](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399) | 一生足迹是一款 iOS 端记录用户足迹的应用, 耗电量较低, 可常驻后台<br>**安装**: [App Store](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399)下载安装即可, 需要开启 iCloud 同步<br>**使用**对于 macOS 用户, 程序会自动查找一生足迹在 iCloud 中的备份位置; 对于其他平台用户, 需要手动指定目录(含`backUpData.csv`文件)的位置 |
| [一生足迹](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399) | 一生足迹是一款 iOS 端记录用户足迹的应用, 耗电量较低, 可常驻后台<br>**安装:** [App Store](https://apps.apple.com/us/app/footprint-record-lifes-path/id1225520399)下载安装即可, 需要开启 iCloud 同步<br>**使用:** 对于 macOS 用户, 程序会自动查找一生足迹在 iCloud 中的备份位置; 对于其他平台用户, 需要手动指定目录(含`backUpData.csv`文件)的位置<br>**注意:** 该备份文件的同步不是很及时, 如果拍摄时间较新需要在 App 中手动到处数据文件加载 |
目前程序默认选项为 `ExifTool` + `一生足迹`, 在 macOS 平台安装 `ExifTool.pkg` 后可直接使用默认参数启动工具
Expand Down Expand Up @@ -93,6 +110,13 @@ Options:

[default: 600]

-c, --location-coordinate-target <LOCATION_COORDINATE_TARGET>
[default: gcj02]

Possible values:
- wgs84: Global coordinate system
- gcj02: China coordinate system

-o, --overwrite-original
Overwrite original file

Expand Down Expand Up @@ -123,6 +147,9 @@ Options:
对于 ExifWriter 和 LocationReader, 请参考 `src/exif_writer``src/location_reader` 目录下已有的实现, 实现对应的 Trait 后在 `src/core/app.rs` 中注册即可
> [!IMPORTANT]
> Location Reader返回的经纬度应该为地球坐标系(WGS84), 本工具会根据用户选择的坐标系进行转换
## License
[MIT](LICENSE) ©[Lyn](mailto://i@lyn.moe)
26 changes: 26 additions & 0 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

<a href="README.md">中文</a> | English

![GitHub release (with filter)](https://img.shields.io/github/v/release/LynMoe/nya-exif)

## Introduction

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

## Features

- [x] Supports JPEG and PNG as well as mainstream RAW formats from major camera manufacturers
- [x] Multi-platform support
- [x] Supports GCJ-02 and WGS-84 coordinate systems (defaults to GCJ-02)

## DEMO

```shell
Expand All @@ -18,6 +26,8 @@
## Usage
Ensure [ExifTool](https://exiftool.org/) is installed and added to PATH.
```shell
# On macOS, you can directly run the "Lifetime Footprints" to start iCloud backup.
nya-exif /path/to/images
Expand All @@ -27,8 +37,14 @@ nya-exif -f /path/to/life-path/data /path/to/images

# If the ExifTool installation path is not in PATH, manually specify the executable file location.
nya-exif -b /path/to/exiftool /path/to/images

# Specify the target coordinate system, default is China's GCJ-02 coordinate system. Needs to be specified as WGS-84 coordinate system if the photo is taken overseas.
nya-exif -c wgs84 /path/to/images
```
> [!NOTE]
> It is recommended to run the program on local files. If running on a network drive, the speed of the program will be affected.
## ExifWriter/LocationReader Table
| Exif Writer | Description |
Expand Down Expand Up @@ -93,6 +109,13 @@ Options:

[default: 600]

-c, --location-coordinate-target <LOCATION_COORDINATE_TARGET>
[default: gcj02]

Possible values:
- wgs84: Global coordinate system
- gcj02: China coordinate system

-o, --overwrite-original
Overwrite original file

Expand Down Expand Up @@ -123,6 +146,9 @@ If you encounter files that ExifTool cannot handle, please attach the file and s
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`.
> [!IMPORTANT]
> 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.
## License
[MIT](LICENSE) ©[Lyn](mailto://i@lyn.moe)
26 changes: 25 additions & 1 deletion src/core/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use clap::ValueEnum;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use simple_log::log::{warn, info};
use undrift_gps::wgs_to_gcj;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use crate::exif_writer::{exiftool::ExifWriterExifTool, ExifWriterBase, ExifWriterParam};
use crate::location_reader::{life_path, LocationReaderBase, LocationReaderParam};
Expand All @@ -19,6 +20,14 @@ pub enum LocationReaderType {
LifePath,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum LocationGpsCoordinateTarget {
/// Global coordinate system
WGS84,
/// China coordinate system
GCJ02,
}

#[derive(Debug)]
pub struct AppParams {
pub operate_dir: PathBuf,
Expand All @@ -28,6 +37,7 @@ pub struct AppParams {
pub location_reader_type: LocationReaderType,
pub location_file_path: Option<PathBuf>,
pub location_max_interval: u32,
pub location_gps_coordinate_target: LocationGpsCoordinateTarget,
pub overwrite_original: bool,
pub time_offset: i32,
}
Expand Down Expand Up @@ -77,7 +87,21 @@ pub fn run(params: AppParams) {

let filename = file.as_str();
let time = exiftool.read_timestamp(filename);
let location = location_reader.get_location(time as i32);
let mut location = location_reader.get_location(time as i32);

match params.location_gps_coordinate_target {
LocationGpsCoordinateTarget::GCJ02 => {
if location.is_some() {
let mut lo = location.unwrap();
let (lat, lon) = wgs_to_gcj(lo.lat, lo.lon);
lo.lat = lat;
lo.lon = lon;

location = Some(lo);
}
}
_ => {}
}

pb.suspend(|| {
let filename = Path::new(filename).file_name().unwrap().to_str().unwrap();
Expand Down
16 changes: 13 additions & 3 deletions src/core/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::PathBuf;
use simple_log::log::debug;
use simple_log::LogConfigBuilder;

use crate::core::app::{self, ExifWriterType, LocationReaderType};
use crate::core::app::{self, ExifWriterType, LocationReaderType, LocationGpsCoordinateTarget};

#[derive(Parser)]
#[command(name = "nya-exif")]
Expand Down Expand Up @@ -51,7 +51,10 @@ struct Cli {
///
/// If the difference between the timestamp of the location data and the photo exceeds this value, the location data will not be written.
#[arg(short = 'i', long, default_value_t = 600)]
location_max_interval: i32,
location_max_interval: u32,

#[arg(short = 'c', long, value_enum, default_value_t = LocationGpsCoordinateTarget::GCJ02)]
location_coordinate_target: LocationGpsCoordinateTarget,

/// Overwrite original file
#[arg(short, long, default_value_t = true)]
Expand Down Expand Up @@ -81,6 +84,7 @@ pub fn run() {
location_reader_type: app::LocationReaderType::LifePath,
location_file_path: None,
location_max_interval: 1800,
location_gps_coordinate_target: app::LocationGpsCoordinateTarget::WGS84,
overwrite_original: false,
time_offset: 0,
};
Expand Down Expand Up @@ -121,11 +125,17 @@ pub fn run() {
param.location_file_path = Some(PathBuf::from(location_file_path));
}

debug!("Value for location_max_interval: {}", cli.location_max_interval);
param.location_max_interval = cli.location_max_interval;

debug!("Value for location_gps_coordinate_target: {:?}", cli.location_coordinate_target);
param.location_gps_coordinate_target = cli.location_coordinate_target;

debug!("Value for overwrite_original: {}", cli.overwrite_original);
param.overwrite_original = cli.overwrite_original;

debug!("Value for time_offset: {}", cli.time_offset);
param.time_offset = cli.time_offset;
param.time_offset = cli.time_offset;

debug!("Value for app params: {:?}", param);

Expand Down
2 changes: 2 additions & 0 deletions src/exif_writer/exiftool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ impl ExifWriterBase for ExifWriterExifTool {
let dt = DateTime::parse_from_str(&time_string, "%Y:%m:%d %H:%M:%S %z")
.expect("Failed to parse date time string");

debug!("Exiftool read timestamp: {}, time_string: {}", dt.timestamp(), time_string);

dt.timestamp()
}

Expand Down
29 changes: 17 additions & 12 deletions src/location_reader/life_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,22 @@ impl LocationReaderBase for LocationReaderLiftPath {
fn get_location(&mut self, timestamp: i32) -> Option<LocationReaderResult> {
let timestamp = timestamp - self.param.time_offset;
let pos = self.find_closest_position(timestamp)?;
self.file.seek(SeekFrom::Start(pos)).unwrap();

self.file.seek(SeekFrom::Start(pos.0)).unwrap();
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.from_reader(&self.file);

let record1 = rdr.records().next()?.unwrap();

self.file.seek(SeekFrom::Start(pos.1)).unwrap();
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.from_reader(&self.file);
let record2 = rdr.records().next()?.unwrap();

debug!("record1: {:?}", record1);
debug!("record2: {:?}", record2);

let d1 = (record1[0].parse::<i32>().unwrap() - timestamp).abs();
let d2 = (record2[0].parse::<i32>().unwrap() - timestamp).abs();
let p1 = (d2 as f64) / (d1 as f64 + d2 as f64);
Expand All @@ -83,7 +90,9 @@ impl LocationReaderBase for LocationReaderLiftPath {
let alt_mid = record1[10].parse::<f64>().unwrap() * p1 + record2[10].parse::<f64>().unwrap() * p2;
let confidence_radius_min = record1[5].parse::<f32>().unwrap().min(record2[5].parse::<f32>().unwrap());

if time_mid - timestamp > self.param.max_interval as i32 {
debug!("time_mid: {}, max interval: {}", time_mid, self.param.max_interval);

if (time_mid - timestamp).abs() > self.param.max_interval as i32 {
return None;
}

Expand All @@ -98,7 +107,7 @@ impl LocationReaderBase for LocationReaderLiftPath {
}

impl LocationReaderLiftPath {
fn find_closest_position(&self, timestamp: i32) -> Option<u64> {
fn find_closest_position(&self, timestamp: i32) -> Option<(u64, u64)> {
let mut left = 0;
let mut right = self.index.len() - 1;

Expand All @@ -111,22 +120,18 @@ impl LocationReaderLiftPath {
}
}

let diff1 = (self.index[left].0 - timestamp).abs();
let diff2 = (timestamp - self.index[left - 1].0).abs();

if diff1 > diff2 {
left -= 1;
}

let diff2 = (timestamp - self.index[left - 1].0).abs();

let mut result = (left, left - 1);

if left + 1 < self.index.len() {
let diff3 = self.index[left + 1].0 - timestamp;
if diff3 > diff2 {
left -= 1;
result = (left, left + 1);
}
}

Some(self.index[left].1)
Some((self.index[result.0].1, self.index[result.1].1))
}
}

0 comments on commit a3cdc97

Please sign in to comment.