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]]
|
||||
name = "chrono"
|
||||
version = "0.4.23"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.45",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -434,7 +437,7 @@ dependencies = [
|
||||
"rand",
|
||||
"sha2 0.10.6",
|
||||
"subtle",
|
||||
"time",
|
||||
"time 0.3.20",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@ -676,7 +679,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -858,6 +861,7 @@ dependencies = [
|
||||
"axum-sessions",
|
||||
"axum-template",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"hex",
|
||||
"image",
|
||||
"kamadak-exif",
|
||||
@ -1020,7 +1024,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
@ -1473,6 +1477,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.20"
|
||||
@ -1727,6 +1742,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "wasi"
|
||||
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-template = { version = "0.14.0", features = ["minijinja"] }
|
||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||
chrono = "0.4.24"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
image = { version = "0.24.5", default-features = false, features = ["jpeg", "webp-encoder"] }
|
||||
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 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_template::{RenderHtml, engine::Engine};
|
||||
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore, aead::{OsRng, rand_core::RngCore, Aead}, XNonce};
|
||||
use chrono::{DateTime, Utc};
|
||||
use error::AppError;
|
||||
use exif::{Tag, Value, Field, In, Exif};
|
||||
use exif::Exif;
|
||||
use image::{imageops::FilterType, DynamicImage};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio::task;
|
||||
@ -17,11 +18,10 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
|
||||
mod error;
|
||||
|
||||
// TODO
|
||||
// * sort images by date
|
||||
// * Cache generated images (+ cache cleanup)
|
||||
// * Polish the css
|
||||
// * Add configuration file
|
||||
// * Support for headlines
|
||||
// * Polish the css
|
||||
// * Cache generated images (+ cache cleanup)
|
||||
|
||||
type TemplateEngine = Engine<minijinja::Environment<'static>>;
|
||||
type ImageDir = PathBuf;
|
||||
@ -87,6 +87,7 @@ async fn main() {
|
||||
pub struct ImageInfo {
|
||||
width: u32,
|
||||
height: u32,
|
||||
created: DateTime<Utc>,
|
||||
#[serde(with = "hex")]
|
||||
encrypted_name: Vec<u8>,
|
||||
#[serde(skip_serializing)]
|
||||
@ -113,7 +114,7 @@ async fn index(
|
||||
// Encrypt image names
|
||||
let chacha_key = chacha20poly1305::Key::from_slice(&secret_key[0..32]);
|
||||
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 mut ciphertext = chacha.encrypt(&nonce, image_info.name.as_bytes())?;
|
||||
|
||||
@ -123,7 +124,7 @@ async fn index(
|
||||
Ok(image_info)
|
||||
}).collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let images = images.into_iter().rev().collect();
|
||||
images.sort_by_key(|entry| Reverse(entry.created));
|
||||
|
||||
Ok(RenderHtml("index", engine, IndexTempalte {
|
||||
title: "Some pictures".into(),
|
||||
@ -231,7 +232,7 @@ fn read_image_size(mut file: impl BufRead+Seek, exif: Option<&Exif>) -> Result<(
|
||||
.into_dimensions()?;
|
||||
|
||||
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) {
|
||||
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
|
||||
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) =>
|
||||
match orientation.value.get_uint(0) {
|
||||
Some(1) => image,
|
||||
@ -284,34 +285,55 @@ fn read_images(directory: &Path) -> Result<Vec<ImageInfo>> {
|
||||
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> {
|
||||
let file = File::open(path)?;
|
||||
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
|
||||
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")?;
|
||||
|
||||
let datetime = 'datetime: {
|
||||
let datetime: Option<DateTime<Utc>> = exif.and_then(|exif| {
|
||||
// First, try to read creation date from EXIF data
|
||||
if let Ok(ref exif) = exif {
|
||||
match exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
|
||||
Some(Field { value: Value::Ascii(value), ..}) if !value.is_empty() => {
|
||||
break 'datetime String::from_utf8_lossy(&value[0]).into_owned()
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
let datetime_without_timezone = exif
|
||||
.get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY)
|
||||
.and_then(extract_exif_string);
|
||||
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
|
||||
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 {
|
||||
width,
|
||||
height,
|
||||
created: datetime.unwrap_or_default(),
|
||||
name: path.file_name().expect("invalid file path").to_owned(),
|
||||
..Default::default()
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user