reorganize

This commit is contained in:
Nick Pegg 2025-05-09 08:23:26 -07:00
parent 9945b9eb7f
commit 4ebaee95cc
3 changed files with 219 additions and 211 deletions

View file

@ -1,9 +1,10 @@
pub mod album_dir;
mod image;
use crate::config::Config;
use album_dir::{AlbumDir, Image};
use anyhow::{Context, anyhow};
use image::imageops::FilterType;
use crate::generate::image::Image;
use album_dir::AlbumDir;
use anyhow::{anyhow, Context};
use indicatif::ProgressBar;
use rayon::prelude::*;
use serde::Serialize;
@ -16,112 +17,7 @@ use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tera::Tera;
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<Image>,
// 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<Breadcrumb>,
// Immediate children of this album
children: Vec<AlbumContext>,
}
impl TryFrom<&AlbumDir> for AlbumContext {
type Error = anyhow::Error;
fn try_from(album: &AlbumDir) -> anyhow::Result<Self> {
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<Breadcrumb> = 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<AlbumContext> = album
.children
.iter()
.map(AlbumContext::try_from)
.collect::<anyhow::Result<Vec<AlbumContext>>>()?;
let images: Vec<Image> = 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<Image>,
next_image: Option<Image>,
}
const IMG_RESIZE_FILTER: ::image::imageops::FilterType = ::image::imageops::FilterType::Lanczos3;
/// Generate an album
///
/// `root_path` is a path to the root directory of the album. `full` if true will regenerate
@ -202,7 +98,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 {} -> {}",
@ -301,6 +197,110 @@ 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<Image>,
/// 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<Breadcrumb>,
/// Immediate children of this album
children: Vec<AlbumContext>,
}
impl TryFrom<&AlbumDir> for AlbumContext {
type Error = anyhow::Error;
fn try_from(album: &AlbumDir) -> anyhow::Result<Self> {
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<Breadcrumb> = 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<AlbumContext> = album
.children
.iter()
.map(AlbumContext::try_from)
.collect::<anyhow::Result<Vec<AlbumContext>>>()?;
let images: Vec<Image> = 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<Image>,
next_image: Option<Image>,
}
#[cfg(test)]
mod tests {
use super::generate;

View file

@ -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,93 +169,6 @@ 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<Self> {
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<String> {
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::*;
@ -314,17 +227,4 @@ 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")
);
}
}

108
src/generate/image.rs Normal file
View file

@ -0,0 +1,108 @@
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<Self> {
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<String> {
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")
);
}
}