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

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

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