Compare commits
2 commits
9945b9eb7f
...
aa57c0d092
Author | SHA1 | Date | |
---|---|---|---|
aa57c0d092 | |||
4ebaee95cc |
3 changed files with 218 additions and 211 deletions
219
src/generate.rs
219
src/generate.rs
|
@ -1,9 +1,10 @@
|
||||||
pub mod album_dir;
|
pub mod album_dir;
|
||||||
|
mod image;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use album_dir::{AlbumDir, Image};
|
use crate::generate::image::Image;
|
||||||
|
use album_dir::AlbumDir;
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use image::imageops::FilterType;
|
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -16,112 +17,7 @@ use std::path::{Path, PathBuf};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
|
|
||||||
const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
const IMG_RESIZE_FILTER: ::image::imageops::FilterType = ::image::imageops::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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate an album
|
/// Generate an album
|
||||||
///
|
///
|
||||||
/// `root_path` is a path to the root directory of the album. `full` if true will regenerate
|
/// `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)
|
fs::hard_link(&img.path, &full_size_path)
|
||||||
.with_context(|| format!("Error creating hard link at {}", full_size_path.display()))?;
|
.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);
|
let thumb_path = output_path.join(&img.thumb_path);
|
||||||
log::info!(
|
log::info!(
|
||||||
"Resizing {} -> {}",
|
"Resizing {} -> {}",
|
||||||
|
@ -246,7 +142,7 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
||||||
.path
|
.path
|
||||||
.join("_templates/*.html")
|
.join("_templates/*.html")
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or(anyhow!("Missing _templates dir in album dir"))?,
|
.ok_or(anyhow!("Album path {} is invalid", album.path.display()))?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("Generating HTML...");
|
println!("Generating HTML...");
|
||||||
|
@ -301,6 +197,109 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
||||||
Ok(())
|
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 = 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<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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::generate;
|
use super::generate;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
use crate::generate::image::Image;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::slice::Iter;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -314,17 +227,4 @@ mod tests {
|
||||||
]);
|
]);
|
||||||
assert_eq!(imgs, expected);
|
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
108
src/generate/image.rs
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue