Sort images by creation date read from EXIF or file modify date
This commit is contained in:
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()
|
||||
})
|
||||
|
Reference in New Issue
Block a user