diff --git a/src/generate.rs b/src/generate.rs index 767350b..62fdbbe 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,10 +1,9 @@ pub mod album_dir; -mod image; use crate::config::Config; -use crate::generate::image::Image; -use album_dir::AlbumDir; +use album_dir::{AlbumDir, Image}; use anyhow::{Context, anyhow}; +use image::imageops::FilterType; use indicatif::ProgressBar; use rayon::prelude::*; use serde::Serialize; @@ -17,7 +16,112 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use tera::Tera; -const IMG_RESIZE_FILTER: ::image::imageops::FilterType = ::image::imageops::FilterType::Lanczos3; +const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3; + +#[derive(Serialize, Debug)] +struct Breadcrumb { + path: PathBuf, + name: String, +} + +/// A Tera context for album pages +#[derive(Serialize, Debug)] +struct AlbumContext { + // Path required to get back to the root album + root_path: PathBuf, + + name: String, + description: String, + + images: Vec, + + // Path to the cover image thumbnail within /slides/, relative to the album dir. Used when + // linking to an album from a parent album + cover_thumbnail_path: PathBuf, + + // list of: + // - relative dir to walk up to root, e.g. "../../.." + // - dir name + breadcrumbs: Vec, + + // Immediate children of this album + children: Vec, +} + +impl TryFrom<&AlbumDir> for AlbumContext { + type Error = anyhow::Error; + + fn try_from(album: &AlbumDir) -> anyhow::Result { + let name: String = match album.path.file_name() { + Some(n) => n.to_string_lossy().to_string(), + None => String::new(), + }; + + // Build breadcrumbs + let mut breadcrumbs: Vec = Vec::new(); + { + let mut album_path = album.path.clone(); + let mut relpath: PathBuf = PathBuf::new(); + while album_path.pop() { + let filename: &OsStr = album_path.file_name().unwrap_or(OsStr::new("Home")); + relpath.push(".."); + breadcrumbs.push(Breadcrumb { + path: relpath.clone(), + name: filename.to_string_lossy().to_string(), + }); + } + } + breadcrumbs.reverse(); + log::debug!("Crumbs for {}: {breadcrumbs:?}", album.path.display()); + + // The first breadcrumb path is the relative path to the root album + let root_path = if !breadcrumbs.is_empty() { + breadcrumbs[0].path.clone() + } else { + PathBuf::new() + }; + + // Make the path to the thumbnail relative to the album that we're in + let cover_thumbnail_path: PathBuf; + if let Some(parent) = album.path.parent() { + cover_thumbnail_path = album.cover.thumb_path.strip_prefix(parent)?.to_path_buf() + } else { + cover_thumbnail_path = album + .cover + .thumb_path + .strip_prefix(&album.path)? + .to_path_buf() + }; + + let children: Vec = album + .children + .iter() + .map(AlbumContext::try_from) + .collect::>>()?; + let images: Vec = album.images.clone(); + + Ok(AlbumContext { + root_path, + name, + description: album.description.clone(), + breadcrumbs, + images, + children, + cover_thumbnail_path, + }) + } +} + +/// A Tera context for slide (individual image) pages +#[derive(Serialize, Debug)] +struct SlideContext { + // Path required to get back to the root album + root_path: PathBuf, + image: Image, + prev_image: Option, + next_image: Option, +} + /// Generate an album /// /// `root_path` is a path to the root directory of the album. `full` if true will regenerate @@ -98,7 +202,7 @@ fn generate_images(config: &Config, album: &AlbumDir, full: bool) -> anyhow::Res fs::hard_link(&img.path, &full_size_path) .with_context(|| format!("Error creating hard link at {}", full_size_path.display()))?; - let orig_image = ::image::open(&img.path)?; + let orig_image = image::open(&img.path)?; let thumb_path = output_path.join(&img.thumb_path); log::info!( "Resizing {} -> {}", @@ -142,7 +246,7 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> { .path .join("_templates/*.html") .to_str() - .ok_or(anyhow!("Album path {} is invalid", album.path.display()))?, + .ok_or(anyhow!("Missing _templates dir in album dir"))?, )?; println!("Generating HTML..."); @@ -197,109 +301,6 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> { Ok(()) } -#[derive(Serialize, Debug)] -struct Breadcrumb { - path: PathBuf, - name: String, -} - -/// A Tera context for album pages -#[derive(Serialize, Debug)] -struct AlbumContext { - /// Path required to get back to the root album - root_path: PathBuf, - - name: String, - description: String, - - images: Vec, - - /// Path to the cover image thumbnail within /slides/, relative to the album dir. Used when - /// linking to an album from a parent album - cover_thumbnail_path: PathBuf, - - /// list of: - /// - relative dir to walk up to root, e.g. "../../.." - /// - dir name - breadcrumbs: Vec, - - /// Immediate children of this album - children: Vec, -} - -impl TryFrom<&AlbumDir> for AlbumContext { - type Error = anyhow::Error; - - fn try_from(album: &AlbumDir) -> anyhow::Result { - let name: String = match album.path.file_name() { - Some(n) => n.to_string_lossy().to_string(), - None => String::new(), - }; - - // Build breadcrumbs - let mut breadcrumbs: Vec = Vec::new(); - { - let mut album_path = album.path.clone(); - let mut relpath: PathBuf = PathBuf::new(); - while album_path.pop() { - let filename: &OsStr = album_path.file_name().unwrap_or(OsStr::new("Home")); - relpath.push(".."); - breadcrumbs.push(Breadcrumb { - path: relpath.clone(), - name: filename.to_string_lossy().to_string(), - }); - } - } - breadcrumbs.reverse(); - log::debug!("Crumbs for {}: {breadcrumbs:?}", album.path.display()); - - // The first breadcrumb path is the relative path to the root album - let root_path = match breadcrumbs.is_empty() { - false => breadcrumbs[0].path.clone(), - true => PathBuf::new(), - }; - - // Make the path to the thumbnail relative to the album that we're in - let cover_thumbnail_path: PathBuf; - if let Some(parent) = album.path.parent() { - cover_thumbnail_path = album.cover.thumb_path.strip_prefix(parent)?.to_path_buf() - } else { - cover_thumbnail_path = album - .cover - .thumb_path - .strip_prefix(&album.path)? - .to_path_buf() - }; - - let children: Vec = album - .children - .iter() - .map(AlbumContext::try_from) - .collect::>>()?; - let images: Vec = album.images.clone(); - - Ok(AlbumContext { - root_path, - name, - description: album.description.clone(), - breadcrumbs, - images, - children, - cover_thumbnail_path, - }) - } -} - -/// A Tera context for slide (individual image) pages -#[derive(Serialize, Debug)] -struct SlideContext { - // Path required to get back to the root album - root_path: PathBuf, - image: Image, - prev_image: Option, - next_image: Option, -} - #[cfg(test)] mod tests { use super::generate; diff --git a/src/generate/album_dir.rs b/src/generate/album_dir.rs index 461debb..a941f12 100644 --- a/src/generate/album_dir.rs +++ b/src/generate/album_dir.rs @@ -1,7 +1,7 @@ -use crate::generate::image::Image; use anyhow::anyhow; use image::ImageReader; use serde::Serialize; +use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; use std::slice::Iter; @@ -169,6 +169,93 @@ impl<'a> Iterator for AlbumImageIter<'a> { } } +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)] +pub struct Image { + /// Path to the image, relative to the root album + pub path: PathBuf, + pub filename: String, + + /// Text description of the image which is displayed below it on the HTML page + pub description: String, + + pub thumb_filename: String, + pub thumb_path: PathBuf, + pub screen_filename: String, + pub screen_path: PathBuf, + pub html_filename: String, + pub html_path: PathBuf, +} + +impl Image { + pub fn new(path: PathBuf, description: String) -> anyhow::Result { + let filename = path + .file_name() + .ok_or(anyhow!( + "Image path {} is missing a filename", + path.display() + ))? + .to_str() + .ok_or(anyhow!("Cannot convert {} to a string", path.display()))? + .to_string(); + let thumb_filename = Self::slide_filename(&path, "thumb", true)?; + let thumb_path = Self::slide_path(&path, &thumb_filename); + let screen_filename = Self::slide_filename(&path, "screen", true)?; + let screen_path = Self::slide_path(&path, &screen_filename); + let html_filename = Self::slide_filename(&path, "html", false)?; + let html_path = Self::slide_path(&path, &html_filename); + + Ok(Image { + path, + description, + filename, + thumb_filename, + thumb_path, + screen_filename, + screen_path, + html_filename, + html_path, + }) + } + + /// Returns the filename for a given slide type. For example if ext = "thumb" and the current + /// filename is "blah.jpg" this will return "blah.thumb.jpg". If keep_ext if false, it would + /// return "blah.thumb" + fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result { + let mut new_ext: OsString = ext.into(); + if keep_ext { + if let Some(e) = path.extension() { + new_ext = OsString::from( + ext.to_string() + + "." + + e.to_str().ok_or(anyhow!( + "Image {} extension is not valid UTF-8", + path.display() + ))?, + ) + } + } + + let new_path = path.with_extension(new_ext); + let new_name = new_path + .file_name() + .ok_or(anyhow!("Image {} missing a file name", path.display()))? + .to_str() + .ok_or(anyhow!("Unable to convert {} to a string", path.display()))? + .to_string(); + + Ok(new_name) + } + + /// Returns the path to the file in the slides dir given the path to the original image + fn slide_path(path: &Path, file_name: &str) -> PathBuf { + let mut new_path = path.to_path_buf(); + new_path.pop(); + new_path.push("slides"); + new_path.push(file_name); + new_path + } +} + #[cfg(test)] mod tests { use super::*; @@ -227,4 +314,17 @@ mod tests { ]); assert_eq!(imgs, expected); } + + #[test] + fn image_paths() { + let img = Image::new(PathBuf::from("foo/bar/image.jpg"), String::new()).unwrap(); + assert_eq!( + img.thumb_path, + PathBuf::from("foo/bar/slides/image.thumb.jpg") + ); + assert_eq!( + img.screen_path, + PathBuf::from("foo/bar/slides/image.screen.jpg") + ); + } } diff --git a/src/generate/image.rs b/src/generate/image.rs deleted file mode 100644 index 1053d3f..0000000 --- a/src/generate/image.rs +++ /dev/null @@ -1,108 +0,0 @@ -use anyhow::anyhow; -use serde::Serialize; -use std::ffi::OsString; -use std::path::{Path, PathBuf}; - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)] -pub struct Image { - /// Path to the image, relative to the root album - pub path: PathBuf, - pub filename: String, - - /// Text description of the image which is displayed below it on the HTML page - pub description: String, - - pub thumb_filename: String, - pub thumb_path: PathBuf, - pub screen_filename: String, - pub screen_path: PathBuf, - pub html_filename: String, - pub html_path: PathBuf, -} -impl Image { - pub fn new(path: PathBuf, description: String) -> anyhow::Result { - let filename = path - .file_name() - .ok_or(anyhow!( - "Image path {} is missing a filename", - path.display() - ))? - .to_str() - .ok_or(anyhow!("Cannot convert {} to a string", path.display()))? - .to_string(); - let thumb_filename = Self::slide_filename(&path, "thumb", true)?; - let thumb_path = Self::slide_path(&path, &thumb_filename); - let screen_filename = Self::slide_filename(&path, "screen", true)?; - let screen_path = Self::slide_path(&path, &screen_filename); - let html_filename = Self::slide_filename(&path, "html", false)?; - let html_path = Self::slide_path(&path, &html_filename); - - Ok(Image { - path, - description, - filename, - thumb_filename, - thumb_path, - screen_filename, - screen_path, - html_filename, - html_path, - }) - } - - /// Returns the filename for a given slide type. For example if ext = "thumb" and the current - /// filename is "blah.jpg" this will return "blah.thumb.jpg". If keep_ext if false, it would - /// return "blah.thumb" - fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result { - let mut new_ext: OsString = ext.into(); - if keep_ext { - if let Some(e) = path.extension() { - new_ext = OsString::from( - ext.to_string() - + "." - + e.to_str().ok_or(anyhow!( - "Image {} extension is not valid UTF-8", - path.display() - ))?, - ) - } - } - - let new_path = path.with_extension(new_ext); - let new_name = new_path - .file_name() - .ok_or(anyhow!("Image {} missing a file name", path.display()))? - .to_str() - .ok_or(anyhow!("Unable to convert {} to a string", path.display()))? - .to_string(); - - Ok(new_name) - } - - /// Returns the path to the file in the slides dir given the path to the original image - fn slide_path(path: &Path, file_name: &str) -> PathBuf { - let mut new_path = path.to_path_buf(); - new_path.pop(); - new_path.push("slides"); - new_path.push(file_name); - new_path - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn image_paths() { - let img = Image::new(PathBuf::from("foo/bar/image.jpg"), String::new()).unwrap(); - assert_eq!( - img.thumb_path, - PathBuf::from("foo/bar/slides/image.thumb.jpg") - ); - assert_eq!( - img.screen_path, - PathBuf::from("foo/bar/slides/image.screen.jpg") - ); - } -}