Sort images by creation date read from EXIF or file modify date
This commit is contained in:
parent
0480943093
commit
9b6a12f3da
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -378,14 +378,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.23"
|
version = "0.4.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"time 0.1.45",
|
||||||
|
"wasm-bindgen",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -434,7 +437,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"sha2 0.10.6",
|
"sha2 0.10.6",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time",
|
"time 0.3.20",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -676,7 +679,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -858,6 +861,7 @@ dependencies = [
|
|||||||
"axum-sessions",
|
"axum-sessions",
|
||||||
"axum-template",
|
"axum-template",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
"kamadak-exif",
|
"kamadak-exif",
|
||||||
@ -1020,7 +1024,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1473,6 +1477,17 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.20"
|
version = "0.3.20"
|
||||||
@ -1727,6 +1742,12 @@ dependencies = [
|
|||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -9,6 +9,7 @@ axum = { version = "0.6.7", features = ["form", "macros"] }
|
|||||||
axum-sessions = "0.4.1"
|
axum-sessions = "0.4.1"
|
||||||
axum-template = { version = "0.14.0", features = ["minijinja"] }
|
axum-template = { version = "0.14.0", features = ["minijinja"] }
|
||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
|
chrono = "0.4.24"
|
||||||
hex = { version = "0.4.3", features = ["serde"] }
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
image = { version = "0.24.5", default-features = false, features = ["jpeg", "webp-encoder"] }
|
image = { version = "0.24.5", default-features = false, features = ["jpeg", "webp-encoder"] }
|
||||||
kamadak-exif = "0.5.5"
|
kamadak-exif = "0.5.5"
|
||||||
|
66
src/main.rs
66
src/main.rs
@ -1,12 +1,13 @@
|
|||||||
use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::{OsStr, OsString}, fs::File, io::{BufReader, Seek, Cursor, BufRead}, time::SystemTime, env::args_os, os::unix::prelude::OsStrExt};
|
use std::{net::SocketAddr, path::{Path, PathBuf}, ffi::{OsStr, OsString}, fs::File, io::{BufReader, Seek, Cursor, BufRead}, env::args_os, os::unix::prelude::OsStrExt, cmp::Reverse};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
|
use axum::{Router, routing::{get, post}, response::{IntoResponse, Redirect}, http::{StatusCode, header}, extract::{self, State}, Form, handler::Handler};
|
||||||
use axum_sessions::{async_session::CookieStore, SessionLayer, extractors::{ReadableSession, WritableSession}};
|
use axum_sessions::{async_session::CookieStore, SessionLayer, extractors::{ReadableSession, WritableSession}};
|
||||||
use axum_template::{RenderHtml, engine::Engine};
|
use axum_template::{RenderHtml, engine::Engine};
|
||||||
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
|
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use error::AppError;
|
use error::AppError;
|
||||||
use exif::{Tag, Value, Field, In, Exif};
|
use exif::Exif;
|
||||||
use image::{imageops::FilterType, DynamicImage};
|
use image::{imageops::FilterType, DynamicImage};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
@ -17,11 +18,10 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
|
|||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// * sort images by date
|
// * Polish the css
|
||||||
// * Cache generated images (+ cache cleanup)
|
|
||||||
// * Add configuration file
|
// * Add configuration file
|
||||||
// * Support for headlines
|
// * Support for headlines
|
||||||
// * Polish the css
|
// * Cache generated images (+ cache cleanup)
|
||||||
|
|
||||||
type TemplateEngine = Engine<minijinja::Environment<'static>>;
|
type TemplateEngine = Engine<minijinja::Environment<'static>>;
|
||||||
type ImageDir = PathBuf;
|
type ImageDir = PathBuf;
|
||||||
@ -87,6 +87,7 @@ async fn main() {
|
|||||||
pub struct ImageInfo {
|
pub struct ImageInfo {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
created: DateTime<Utc>,
|
||||||
#[serde(with = "hex")]
|
#[serde(with = "hex")]
|
||||||
encrypted_name: Vec<u8>,
|
encrypted_name: Vec<u8>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
@ -113,7 +114,7 @@ async fn index(
|
|||||||
// Encrypt image names
|
// Encrypt image names
|
||||||
let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]);
|
let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]);
|
||||||
let chacha = XChaCha20Poly1305::new(chacha_key);
|
let chacha = XChaCha20Poly1305::new(chacha_key);
|
||||||
let images = images.into_iter().map(|mut image_info| {
|
let mut images = images.into_iter().map(|mut image_info| {
|
||||||
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||||
let mut ciphertext = chacha.encrypt(&nonce, image_info.name.as_bytes())?;
|
let mut ciphertext = chacha.encrypt(&nonce, image_info.name.as_bytes())?;
|
||||||
|
|
||||||
@ -123,7 +124,7 @@ async fn index(
|
|||||||
Ok(image_info)
|
Ok(image_info)
|
||||||
}).collect::<Result<Vec<_>>>()?;
|
}).collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
let images = images.into_iter().rev().collect();
|
images.sort_by_key(|entry| Reverse(entry.created));
|
||||||
|
|
||||||
Ok(RenderHtml("index", engine, IndexTempalte {
|
Ok(RenderHtml("index", engine, IndexTempalte {
|
||||||
title: "Some pictures".into(),
|
title: "Some pictures".into(),
|
||||||
@ -231,7 +232,7 @@ fn read_image_size(mut file: impl BufRead+Seek, exif: Option<&Exif>) -> Result<(
|
|||||||
.into_dimensions()?;
|
.into_dimensions()?;
|
||||||
|
|
||||||
if let Some(exif) = exif {
|
if let Some(exif) = exif {
|
||||||
if let Some(orientation) = exif.get_field(Tag::Orientation, In::PRIMARY) {
|
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||||
if let Some(5 | 6 | 7 | 8) = orientation.value.get_uint(0) {
|
if let Some(5 | 6 | 7 | 8) = orientation.value.get_uint(0) {
|
||||||
std::mem::swap(&mut width, &mut height);
|
std::mem::swap(&mut width, &mut height);
|
||||||
};
|
};
|
||||||
@ -243,7 +244,7 @@ fn read_image_size(mut file: impl BufRead+Seek, exif: Option<&Exif>) -> Result<(
|
|||||||
|
|
||||||
// How many degrees to rotate clockwise, does not support flipped images
|
// How many degrees to rotate clockwise, does not support flipped images
|
||||||
fn fix_image_orientation(image: DynamicImage, exif: &Exif) -> DynamicImage {
|
fn fix_image_orientation(image: DynamicImage, exif: &Exif) -> DynamicImage {
|
||||||
match exif.get_field(Tag::Orientation, In::PRIMARY) {
|
match exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
|
||||||
Some(orientation) =>
|
Some(orientation) =>
|
||||||
match orientation.value.get_uint(0) {
|
match orientation.value.get_uint(0) {
|
||||||
Some(1) => image,
|
Some(1) => image,
|
||||||
@ -284,34 +285,55 @@ fn read_images(directory: &Path) -> Result<Vec<ImageInfo>> {
|
|||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_exif_string(field: &exif::Field) -> Option<String> {
|
||||||
|
match field.value {
|
||||||
|
exif::Value::Ascii(ref value) if !value.is_empty() => {
|
||||||
|
String::from_utf8(value[0].clone()).ok()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_image_info(path: &Path) -> Result<ImageInfo> {
|
fn read_image_info(path: &Path) -> Result<ImageInfo> {
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mut file = BufReader::new(file);
|
let mut file = BufReader::new(file);
|
||||||
|
|
||||||
let exif = read_exif_data(&mut file);
|
let exif = read_exif_data(&mut file).ok();
|
||||||
|
|
||||||
// Check if we need to flip the coordinates
|
// Check if we need to flip the coordinates
|
||||||
let (width, height) = read_image_size(&mut file, exif.as_ref().ok())
|
let (width, height) = read_image_size(&mut file, exif.as_ref())
|
||||||
.context("Could not read image size")?;
|
.context("Could not read image size")?;
|
||||||
|
|
||||||
let datetime = 'datetime: {
|
let datetime: Option<DateTime<Utc>> = exif.and_then(|exif| {
|
||||||
// First, try to read creation date from EXIF data
|
// First, try to read creation date from EXIF data
|
||||||
if let Ok(ref exif) = exif {
|
let datetime_without_timezone = exif
|
||||||
match exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
|
.get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY)
|
||||||
Some(Field { value: Value::Ascii(value), ..}) if !value.is_empty() => {
|
.and_then(extract_exif_string);
|
||||||
break 'datetime String::from_utf8_lossy(&value[0]).into_owned()
|
let timezone = exif
|
||||||
}
|
.get_field(exif::Tag::OffsetTimeOriginal, exif::In::PRIMARY)
|
||||||
_ => {}
|
.and_then(extract_exif_string);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
match (datetime_without_timezone, timezone) {
|
||||||
|
(Some(datetime), Some(timezone)) => (datetime + &timezone).parse().ok(),
|
||||||
|
(Some(datetime), None) => (datetime + "Z").parse().ok(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}).or_else(|| {
|
||||||
// If that doesn't work, fall back to the file modification time
|
// If that doesn't work, fall back to the file modification time
|
||||||
format!("{:?}", std::fs::metadata(path).unwrap().modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap())
|
std::fs::metadata(path)
|
||||||
};
|
.and_then(|m| m.modified())
|
||||||
|
.map(DateTime::from)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
if datetime.is_none() {
|
||||||
|
tracing::warn!("Could not determine original datetime for {:?}", path);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ImageInfo {
|
Ok(ImageInfo {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
created: datetime.unwrap_or_default(),
|
||||||
name: path.file_name().expect("invalid file path").to_owned(),
|
name: path.file_name().expect("invalid file path").to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user