Sort images by creation date read from EXIF or file modify date

This commit is contained in:
Klemens Schölhorn 2023-03-13 23:10:14 +01:00
parent 0480943093
commit 9b6a12f3da
3 changed files with 71 additions and 27 deletions

31
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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()
}) })