From 5d5c988ba400cea00d42df56f1937168936b8f76 Mon Sep 17 00:00:00 2001 From: Nick Pegg Date: Sun, 4 May 2025 20:39:21 -0700 Subject: [PATCH] add image generation --- src/config.rs | 3 + src/generate.rs | 40 +++++++++- src/generate/album_dir.rs | 97 ++++++++++++++++++++----- src/main.rs | 2 +- tests/{generate.rs => test_generate.rs} | 4 +- 5 files changed, 123 insertions(+), 23 deletions(-) rename tests/{generate.rs => test_generate.rs} (96%) diff --git a/src/config.rs b/src/config.rs index 10157b1..f5ab9df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,8 +6,11 @@ use std::path::PathBuf; #[derive(Deserialize, Debug, PartialEq)] #[serde(default)] pub struct Config { + /// Tuple of how big thumbnails should be - (width, height) pub thumbnail_size: (u32, u32), + /// Tuple of how big thumbnails should be - (width, height) pub view_size: (u32, u32), + /// Directory inside the album that the site should be output to pub output_dir: PathBuf, } diff --git a/src/generate.rs b/src/generate.rs index 7a7624a..952dd4e 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -2,14 +2,21 @@ mod album_dir; use crate::config::Config; pub use album_dir::AlbumDir; +use image::imageops::FilterType; use std::env; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; + +const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3; pub fn generate(root_path: &PathBuf) -> anyhow::Result { + log::debug!("Generating album in {}", root_path.display()); let config = Config::from_album(root_path.to_path_buf())?; let orig_path = env::current_dir()?; + + // Jump into the root path so that all paths are relative to the root of the album + env::set_current_dir(root_path)?; let album = AlbumDir::try_from(root_path)?; - env::set_current_dir(&root_path)?; generate_images(&config, &album)?; generate_html(&config, &album)?; @@ -19,6 +26,35 @@ pub fn generate(root_path: &PathBuf) -> anyhow::Result { } fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> { + let output_path = album.path.join(&config.output_dir); + // TODO: use par_iter() ? + // TODO: progress bar ? + for img in album.iter() { + let orig_image = image::open(&img.path)?; + + let thumb_path = output_path.join(img.thumb_path()?); + fs::create_dir_all(thumb_path.parent().unwrap_or(Path::new("")))?; + orig_image + .resize( + config.thumbnail_size.0, + config.thumbnail_size.1, + IMG_RESIZE_FILTER, + ) + .save(&thumb_path)?; + log::info!("Resized {} -> {}", img.path.display(), thumb_path.display()); + + // TODO: resize to screen size + let screen_path = output_path.join(img.screen_path()?); + fs::create_dir_all(thumb_path.parent().unwrap_or(Path::new("")))?; + orig_image + .resize(config.view_size.0, config.view_size.1, IMG_RESIZE_FILTER) + .save(&screen_path)?; + log::info!( + "Resized {} -> {}", + img.path.display(), + screen_path.display() + ); + } Ok(()) } diff --git a/src/generate/album_dir.rs b/src/generate/album_dir.rs index df460ea..2e51800 100644 --- a/src/generate/album_dir.rs +++ b/src/generate/album_dir.rs @@ -1,7 +1,7 @@ +use anyhow::anyhow; use image::ImageReader; -use std::ffi::OsString; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::slice::Iter; /// An album directory, which has images and possibly child albums @@ -18,21 +18,20 @@ pub struct AlbumDir { impl AlbumDir { /// Returns an iterator over all images in the album and subalbums - fn iter(&self) -> AlbumIter { + pub fn iter(&self) -> AlbumIter { AlbumIter::new(self) } -} -impl TryFrom<&PathBuf> for AlbumDir { - type Error = anyhow::Error; - - fn try_from(p: &PathBuf) -> anyhow::Result { + /// Create an AlbumDir recursively from a path. The root path is so that we can make every path + /// relative to the root. + fn from_path(p: &Path, root: &Path) -> anyhow::Result { let mut images = vec![]; let mut children = vec![]; - let mut description = "".to_string(); + let mut description = String::new(); for entry in p.read_dir()? { - let entry_path = entry?.path(); + // use strip_prefix() to make the path relative to the root directory + let entry_path = entry?.path().strip_prefix(root)?.to_path_buf(); if entry_path.is_file() { if let Some(filename) = entry_path.file_name() { @@ -56,6 +55,7 @@ impl TryFrom<&PathBuf> for AlbumDir { // TODO: render markdown todo!(); } + images.push(Image { path: entry_path, description, @@ -81,7 +81,7 @@ impl TryFrom<&PathBuf> for AlbumDir { // Find all directories in directory and make AlbumDirs out of them, // but skip dirs known to have interesting stuff Ok(AlbumDir { - path: p.clone(), + path: p.strip_prefix(root)?.to_path_buf(), images, children, description, @@ -89,11 +89,16 @@ impl TryFrom<&PathBuf> for AlbumDir { } } -// TODO: from-path, find all images and children +impl TryFrom<&PathBuf> for AlbumDir { + type Error = anyhow::Error; + + fn try_from(p: &PathBuf) -> anyhow::Result { + AlbumDir::from_path(p, p) + } +} /// An iterator which walks through all of the images in an album, and its sub-albums -struct AlbumIter<'a> { - root_album: &'a AlbumDir, +pub struct AlbumIter<'a> { image_iter: Box + 'a>, children_iter: Iter<'a, AlbumDir>, } @@ -101,7 +106,6 @@ struct AlbumIter<'a> { impl<'a> AlbumIter<'a> { fn new(ad: &'a AlbumDir) -> Self { Self { - root_album: ad, image_iter: Box::new(ad.images.iter()), children_iter: ad.children.iter(), } @@ -131,9 +135,50 @@ impl<'a> Iterator for AlbumIter<'a> { } #[derive(Clone, Hash, PartialEq, Eq)] -struct Image { - path: PathBuf, - description: String, +pub struct Image { + /// Path to the image, relative to the root album + pub path: PathBuf, + + /// Text description of the image which is displayed below it on the HTML page + pub description: String, +} + +impl Image { + pub fn thumb_path(&self) -> anyhow::Result { + self.slide_path("thumb") + } + + pub fn screen_path(&self) -> anyhow::Result { + self.slide_path("screen") + } + + /// Returns the path to the file in the slides dir with the given extention insert, e.g. + /// "thumb" or "display" + fn slide_path(&self, ext: &str) -> anyhow::Result { + // TODO: Return path relative to the output dir? + let new_ext = match self.path.extension() { + Some(e) => { + ext.to_string() + + "." + + e.to_str().ok_or(anyhow!( + "Image {} extension is not valid UTF-8", + self.path.display() + ))? + } + None => ext.to_string(), + }; + + let new_path = self.path.with_extension(new_ext); + let new_name = new_path + .file_name() + .ok_or(anyhow!("Image {} missing a file name", self.path.display()))?; + let parent = self + .path + .parent() + .ok_or(anyhow!("Image {} has no parent dir", self.path.display()))?; + + Ok(parent.join("slides").join(new_name)) + } } #[cfg(test)] @@ -200,4 +245,20 @@ mod tests { ]); assert_eq!(imgs, expected); } + + #[test] + fn image_paths() { + let img = Image { + path: PathBuf::from("foo/bar/image.jpg"), + description: String::new(), + }; + assert_eq!( + img.thumb_path().unwrap(), + PathBuf::from("foo/bar/slides/image.thumb.jpg") + ); + assert_eq!( + img.screen_path().unwrap(), + PathBuf::from("foo/bar/slides/image.screen.jpg") + ); + } } diff --git a/src/main.rs b/src/main.rs index a1cbc4b..5b53e23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> { } Commands::Generate { quick } => { println!("Generate, quick: {quick}"); - generate(&album_path.to_path_buf()); + generate(&album_path.to_path_buf())?; } } diff --git a/tests/generate.rs b/tests/test_generate.rs similarity index 96% rename from tests/generate.rs rename to tests/test_generate.rs index fb68be1..d4d86ed 100644 --- a/tests/generate.rs +++ b/tests/test_generate.rs @@ -36,7 +36,7 @@ fn make_test_album() -> Temp { let dest_path = tmpdir.join(&path_in_album); fs::create_dir_all(dest_path.parent().unwrap()).unwrap(); fs::copy(&entry_path, &dest_path).unwrap(); - log::debug!("{} -> {}", entry_path.display(), dest_path.display()); + log::debug!("Copied {} -> {}", entry_path.display(), dest_path.display()); } } } @@ -52,7 +52,7 @@ fn check_album(album_dir: PathBuf) -> anyhow::Result<()> { while let Some(dir) = dirs.pop_front() { for entry in dir.read_dir().unwrap() { let path = entry.unwrap().path(); - if path.is_dir() { + if path.is_dir() && !path.ends_with(Path::new("slides")) { check_album(path.clone())?; } let files: Vec = path